Fun changes
This commit is contained in:
parent
cd778dd088
commit
349a6c1d53
60 changed files with 1260 additions and 2600 deletions
|
|
@ -4,30 +4,43 @@ import { ENUM_PALETTE } from './consts';
|
|||
|
||||
/**
|
||||
* LayerExtension that turns polygon fills into pie charts.
|
||||
* Injects a fragment shader that computes angle from each fragment's position
|
||||
* to the polygon centroid, then picks a slice color from the enum palette.
|
||||
*
|
||||
* Works with H3HexagonLayer (hex fills) and GeoJsonLayer (postcode fills).
|
||||
* Only activates on SolidPolygonLayer sublayers (fill), not PathLayer (stroke).
|
||||
* Follows the canonical deck.gl v9 extension pattern:
|
||||
* - defaultProps with type:'accessor' enables getSubLayerProps() to wrap
|
||||
* accessors via getSubLayerAccessor(), which unwraps __source.object to
|
||||
* access the original data item through CompositeLayer sublayer chains.
|
||||
* - stepMode:'dynamic' handles per-instance counting automatically.
|
||||
* - isEnabled() restricts to SolidPolygonLayer (fill) sublayers only.
|
||||
*
|
||||
* Required layer props when this extension is active:
|
||||
* getCenter: (d) => [lon, lat] — polygon centroid in world coordinates
|
||||
* getRatios0: (d) => number[4] — pie ratios for slices 0-3
|
||||
* getRatios1: (d) => number[4] — pie ratios for slices 4-7
|
||||
* getRatios2: (d) => number[2] — pie ratios for slices 8-9
|
||||
* Accepts an optional custom palette in the constructor for per-feature color overrides.
|
||||
*/
|
||||
|
||||
// Build palette as GLSL vec3 constants (normalized 0-1)
|
||||
const PALETTE_GLSL = ENUM_PALETTE.map(
|
||||
(c) =>
|
||||
`vec3(${(c[0] / 255).toFixed(4)}, ${(c[1] / 255).toFixed(4)}, ${(c[2] / 255).toFixed(4)})`
|
||||
).join(',\n ');
|
||||
function paletteToGlsl(palette: [number, number, number][]): string {
|
||||
return palette
|
||||
.map(
|
||||
(c) =>
|
||||
`vec3(${(c[0] / 255).toFixed(4)}, ${(c[1] / 255).toFixed(4)}, ${(c[2] / 255).toFixed(4)})`
|
||||
)
|
||||
.join(',\n ');
|
||||
}
|
||||
|
||||
export class PieHexExtension extends LayerExtension {
|
||||
static extensionName = 'PieHexExtension';
|
||||
static defaultProps = {
|
||||
getCenter: { type: 'accessor', value: [0, 0] },
|
||||
getRatios0: { type: 'accessor', value: [1, 0, 0, 0] },
|
||||
getRatios1: { type: 'accessor', value: [0, 0, 0, 0] },
|
||||
getRatios2: { type: 'accessor', value: [0, 0] },
|
||||
};
|
||||
|
||||
private paletteGlsl: string;
|
||||
|
||||
constructor(palette?: [number, number, number][]) {
|
||||
super();
|
||||
this.paletteGlsl = paletteToGlsl(palette ?? ENUM_PALETTE);
|
||||
}
|
||||
|
||||
isEnabled(layer: any): boolean {
|
||||
// Only apply to fill sublayers (SolidPolygonLayer), not stroke (PathLayer)
|
||||
return layer.id.endsWith('-fill');
|
||||
}
|
||||
|
||||
|
|
@ -61,7 +74,7 @@ in vec4 vRatios0;
|
|||
in vec4 vRatios1;
|
||||
in vec2 vRatios2;
|
||||
const vec3 pieColors[10] = vec3[10](
|
||||
${PALETTE_GLSL}
|
||||
${this.paletteGlsl}
|
||||
);`,
|
||||
'fs:DECKGL_FILTER_COLOR': `\
|
||||
{
|
||||
|
|
@ -98,25 +111,25 @@ const vec3 pieColors[10] = vec3[10](
|
|||
if (!extension.isEnabled(this)) return;
|
||||
const am = this.getAttributeManager();
|
||||
if (!am) return;
|
||||
am.addInstanced({
|
||||
am.add({
|
||||
instancePieCenter: {
|
||||
size: 2,
|
||||
type: 'float32',
|
||||
stepMode: 'dynamic',
|
||||
accessor: 'getCenter',
|
||||
},
|
||||
instanceRatios0: {
|
||||
size: 4,
|
||||
type: 'float32',
|
||||
stepMode: 'dynamic',
|
||||
accessor: 'getRatios0',
|
||||
},
|
||||
instanceRatios1: {
|
||||
size: 4,
|
||||
type: 'float32',
|
||||
stepMode: 'dynamic',
|
||||
accessor: 'getRatios1',
|
||||
},
|
||||
instanceRatios2: {
|
||||
size: 2,
|
||||
type: 'float32',
|
||||
stepMode: 'dynamic',
|
||||
accessor: 'getRatios2',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -109,6 +109,8 @@ export const STACKED_GROUPS: Record<
|
|||
label: string;
|
||||
/** If set, use this feature's stats for the total and info popup. Otherwise sum components. */
|
||||
feature?: string;
|
||||
/** If set, display this feature's mean as the primary value (e.g. per-1k rate) instead of the absolute total. */
|
||||
rateFeature?: string;
|
||||
/** Suffix shown after the total value (e.g. "avg/yr") */
|
||||
unit?: string;
|
||||
/** Feature names that make up the segments */
|
||||
|
|
@ -119,7 +121,8 @@ export const STACKED_GROUPS: Record<
|
|||
{
|
||||
label: 'Serious crime',
|
||||
feature: 'Serious crime (avg/yr)',
|
||||
unit: 'avg/yr',
|
||||
rateFeature: 'Serious crime per 1k residents (avg/yr)',
|
||||
unit: 'per 1k/yr',
|
||||
components: [
|
||||
'Violence and sexual offences (avg/yr)',
|
||||
'Robbery (avg/yr)',
|
||||
|
|
@ -130,7 +133,8 @@ export const STACKED_GROUPS: Record<
|
|||
{
|
||||
label: 'Minor crime',
|
||||
feature: 'Minor crime (avg/yr)',
|
||||
unit: 'avg/yr',
|
||||
rateFeature: 'Minor crime per 1k residents (avg/yr)',
|
||||
unit: 'per 1k/yr',
|
||||
components: [
|
||||
'Anti-social behaviour (avg/yr)',
|
||||
'Criminal damage and arson (avg/yr)',
|
||||
|
|
@ -179,7 +183,7 @@ export const STACKED_ENUM_GROUPS: Record<
|
|||
feature: 'Property type',
|
||||
components: ['Property type'],
|
||||
valueOrder: ['Detached', 'Semi-Detached', 'Terraced', 'Flats/Maisonettes', 'Other'],
|
||||
valueColors: ['#8b5cf6', '#3b82f6', '#14b8a6', '#f59e0b', '#6b7280'],
|
||||
valueColors: ['#f97316', '#3b82f6', '#22c55e', '#ec4899', '#6b7280'],
|
||||
},
|
||||
{
|
||||
label: 'Leasehold/Freehold',
|
||||
|
|
@ -208,6 +212,60 @@ export const ENUM_PALETTE: [number, number, number][] = [
|
|||
[107, 114, 128], // gray-500
|
||||
];
|
||||
|
||||
/**
|
||||
* Per-feature color overrides for enum values on the map and dashboard.
|
||||
* Keys are feature names (as returned by the server), values map enum value → RGB.
|
||||
* Any value not listed falls back to ENUM_PALETTE by index.
|
||||
*/
|
||||
export const ENUM_COLOR_OVERRIDES: Record<string, Record<string, [number, number, number]>> = {
|
||||
'Winning party': {
|
||||
Labour: [220, 36, 31], // Labour red
|
||||
Conservative: [0, 135, 220], // Conservative blue
|
||||
'Liberal Democrat': [253, 187, 48], // Lib Dem gold
|
||||
'Reform UK': [18, 178, 196], // Reform teal
|
||||
Green: [106, 176, 35], // Green party green
|
||||
'Other parties': [148, 130, 160], // muted purple
|
||||
},
|
||||
'Property type': {
|
||||
Detached: [249, 115, 22], // orange
|
||||
'Semi-Detached': [59, 130, 246], // blue
|
||||
Terraced: [34, 197, 94], // green
|
||||
'Flats/Maisonettes': [236, 72, 153], // pink
|
||||
Other: [107, 114, 128], // gray
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Build a 10-color palette for a given feature, using overrides where defined.
|
||||
* Returns the default ENUM_PALETTE when no overrides exist.
|
||||
*/
|
||||
export function getEnumPaletteForFeature(
|
||||
featureName: string | null,
|
||||
values?: string[]
|
||||
): [number, number, number][] {
|
||||
if (!featureName || !values) return ENUM_PALETTE;
|
||||
const overrides = ENUM_COLOR_OVERRIDES[featureName];
|
||||
if (!overrides) return ENUM_PALETTE;
|
||||
|
||||
const palette: [number, number, number][] = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
if (i < values.length && overrides[values[i]]) {
|
||||
palette.push(overrides[values[i]]);
|
||||
} else {
|
||||
palette.push(ENUM_PALETTE[i % ENUM_PALETTE.length]);
|
||||
}
|
||||
}
|
||||
return palette;
|
||||
}
|
||||
|
||||
/** Look up override color for a specific enum value, or null if none. */
|
||||
export function getEnumValueColor(
|
||||
featureName: string,
|
||||
valueName: string
|
||||
): [number, number, number] | null {
|
||||
return ENUM_COLOR_OVERRIDES[featureName]?.[valueName] ?? null;
|
||||
}
|
||||
|
||||
/** Colors for stacked bar segments */
|
||||
export const SEGMENT_COLORS = [
|
||||
'#ef4444', // red-500
|
||||
|
|
|
|||
|
|
@ -49,12 +49,6 @@ const RIGHTMOVE_PRICES = [
|
|||
3000000, 4000000, 5000000, 7500000, 10000000, 15000000, 20000000,
|
||||
];
|
||||
|
||||
// Rightmove allowed monthly rent values (pcm)
|
||||
const RIGHTMOVE_RENTS = [
|
||||
250, 300, 350, 400, 450, 500, 600, 700, 800, 900, 1000, 1250, 1500, 1750, 2000, 2500, 3000, 3500,
|
||||
4000, 5000, 7500, 10000, 15000, 25000,
|
||||
];
|
||||
|
||||
// OnTheMarket allowed buy prices
|
||||
const OTM_PRICES = [
|
||||
50000, 60000, 70000, 80000, 90000, 100000, 110000, 120000, 125000, 130000, 140000, 150000, 160000,
|
||||
|
|
@ -64,12 +58,6 @@ const OTM_PRICES = [
|
|||
10000000, 15000000,
|
||||
];
|
||||
|
||||
// OnTheMarket allowed monthly rent values (pcm)
|
||||
const OTM_RENTS = [
|
||||
100, 200, 250, 300, 350, 400, 450, 500, 550, 600, 650, 700, 750, 800, 850, 900, 950, 1000, 1100,
|
||||
1200, 1250, 1300, 1400, 1500, 1750, 2000, 2500, 3000, 3500, 4000, 5000, 7500, 10000, 25000,
|
||||
];
|
||||
|
||||
// Zoopla allowed buy prices
|
||||
const ZOOPLA_PRICES = [
|
||||
10000, 25000, 50000, 75000, 100000, 125000, 150000, 175000, 200000, 225000, 250000, 275000,
|
||||
|
|
@ -78,12 +66,6 @@ const ZOOPLA_PRICES = [
|
|||
5000000, 7500000, 10000000, 15000000,
|
||||
];
|
||||
|
||||
// Zoopla allowed monthly rent values (pcm)
|
||||
const ZOOPLA_RENTS = [
|
||||
100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1250, 1500, 1750, 2000, 2500, 3000, 3500, 4000,
|
||||
5000, 7500, 10000, 25000,
|
||||
];
|
||||
|
||||
function snapToAllowed(value: number, allowed: number[], direction: 'floor' | 'ceil'): number {
|
||||
if (direction === 'floor') {
|
||||
for (let i = allowed.length - 1; i >= 0; i--) {
|
||||
|
|
@ -115,26 +97,14 @@ export function buildPropertySearchUrls({
|
|||
rightmove: string | null;
|
||||
onthemarket: string;
|
||||
zoopla: string;
|
||||
openrent: string | null;
|
||||
} | null {
|
||||
const { postcode, resolution, isPostcode } = location;
|
||||
if (!postcode) return null;
|
||||
|
||||
const radiusMiles = isPostcode ? 0.25 : (H3_RADIUS_MILES[resolution] ?? 1);
|
||||
const radiusMiles = isPostcode ? 0 : (H3_RADIUS_MILES[resolution] ?? 1);
|
||||
|
||||
const listingStatus = filters['Listing status'];
|
||||
const isRent =
|
||||
Array.isArray(listingStatus) &&
|
||||
typeof listingStatus[0] === 'string' &&
|
||||
(listingStatus as string[]).includes('For rent');
|
||||
|
||||
// Check price filters in priority order: asking price (current listings) > estimated > last known
|
||||
// For rent mode, check asking rent first
|
||||
const priceFilter = isRent
|
||||
? filters['Asking rent (monthly)']
|
||||
: (filters['Asking price'] ??
|
||||
filters['Estimated current price'] ??
|
||||
filters['Last known price']);
|
||||
const priceFilter =
|
||||
filters['Estimated current price'] ?? filters['Last known price'];
|
||||
const minPrice =
|
||||
Array.isArray(priceFilter) && typeof priceFilter[0] === 'number' ? priceFilter[0] : undefined;
|
||||
const maxPrice =
|
||||
|
|
@ -146,26 +116,6 @@ export function buildPropertySearchUrls({
|
|||
? (propertyTypes as string[])
|
||||
: [];
|
||||
|
||||
const bedroomFilter = filters['Bedrooms'];
|
||||
const minBedrooms =
|
||||
Array.isArray(bedroomFilter) && typeof bedroomFilter[0] === 'number'
|
||||
? bedroomFilter[0]
|
||||
: undefined;
|
||||
const maxBedrooms =
|
||||
Array.isArray(bedroomFilter) && typeof bedroomFilter[1] === 'number'
|
||||
? bedroomFilter[1]
|
||||
: undefined;
|
||||
|
||||
const bathroomFilter = filters['Bathrooms'];
|
||||
const minBathrooms =
|
||||
Array.isArray(bathroomFilter) && typeof bathroomFilter[0] === 'number'
|
||||
? bathroomFilter[0]
|
||||
: undefined;
|
||||
const maxBathrooms =
|
||||
Array.isArray(bathroomFilter) && typeof bathroomFilter[1] === 'number'
|
||||
? bathroomFilter[1]
|
||||
: undefined;
|
||||
|
||||
const tenureFilter = filters['Leasehold/Freehold'];
|
||||
const selectedTenures =
|
||||
Array.isArray(tenureFilter) && typeof tenureFilter[0] === 'string'
|
||||
|
|
@ -175,20 +125,15 @@ export function buildPropertySearchUrls({
|
|||
// Rightmove — requires locationIdentifier from typeahead API
|
||||
let rightmove: string | null = null;
|
||||
if (rightmoveLocationId) {
|
||||
const rmPrices = isRent ? RIGHTMOVE_RENTS : RIGHTMOVE_PRICES;
|
||||
const rmParams = new URLSearchParams();
|
||||
rmParams.set('searchLocation', postcode);
|
||||
rmParams.set('useLocationIdentifier', 'true');
|
||||
rmParams.set('locationIdentifier', rightmoveLocationId);
|
||||
rmParams.set('radius', String(nearestRadius(radiusMiles, RIGHTMOVE_RADII)));
|
||||
if (minPrice !== undefined)
|
||||
rmParams.set('minPrice', String(snapToAllowed(minPrice, rmPrices, 'floor')));
|
||||
rmParams.set('minPrice', String(snapToAllowed(minPrice, RIGHTMOVE_PRICES, 'floor')));
|
||||
if (maxPrice !== undefined)
|
||||
rmParams.set('maxPrice', String(snapToAllowed(maxPrice, rmPrices, 'ceil')));
|
||||
if (minBedrooms !== undefined) rmParams.set('minBedrooms', String(Math.floor(minBedrooms)));
|
||||
if (maxBedrooms !== undefined) rmParams.set('maxBedrooms', String(Math.ceil(maxBedrooms)));
|
||||
if (minBathrooms !== undefined) rmParams.set('minBathrooms', String(Math.floor(minBathrooms)));
|
||||
if (maxBathrooms !== undefined) rmParams.set('maxBathrooms', String(Math.ceil(maxBathrooms)));
|
||||
rmParams.set('maxPrice', String(snapToAllowed(maxPrice, RIGHTMOVE_PRICES, 'ceil')));
|
||||
if (selectedTypes.length > 0) {
|
||||
const rmTypes = [
|
||||
...new Set(
|
||||
|
|
@ -200,24 +145,22 @@ export function buildPropertySearchUrls({
|
|||
];
|
||||
if (rmTypes.length > 0) rmParams.set('propertyTypes', rmTypes.join(','));
|
||||
}
|
||||
if (!isRent && selectedTenures.length > 0) {
|
||||
if (selectedTenures.length > 0) {
|
||||
const rmTenures = selectedTenures.map((t) => (t === 'Freehold' ? 'FREEHOLD' : 'LEASEHOLD'));
|
||||
rmParams.set('tenureTypes', rmTenures.join(','));
|
||||
}
|
||||
if (!isRent) rmParams.set('_includeSSTC', 'on');
|
||||
const rmPath = isRent ? 'property-to-rent' : 'property-for-sale';
|
||||
rightmove = `https://www.rightmove.co.uk/${rmPath}/find.html?${rmParams.toString()}`;
|
||||
rmParams.set('_includeSSTC', 'on');
|
||||
rightmove = `https://www.rightmove.co.uk/property-for-sale/find.html?${rmParams.toString()}`;
|
||||
}
|
||||
|
||||
// OnTheMarket — postcode slug in URL path (e.g. "SW1A 1AA" → "sw1a-1aa")
|
||||
const otmSlug = postcode.toLowerCase().replace(/\s+/g, '-');
|
||||
const otmPrices = isRent ? OTM_RENTS : OTM_PRICES;
|
||||
const otmParams = new URLSearchParams();
|
||||
otmParams.set('radius', String(nearestRadius(radiusMiles, OTM_RADII)));
|
||||
if (minPrice !== undefined)
|
||||
otmParams.set('min-price', String(snapToAllowed(minPrice, otmPrices, 'floor')));
|
||||
otmParams.set('min-price', String(snapToAllowed(minPrice, OTM_PRICES, 'floor')));
|
||||
if (maxPrice !== undefined)
|
||||
otmParams.set('max-price', String(snapToAllowed(maxPrice, otmPrices, 'ceil')));
|
||||
otmParams.set('max-price', String(snapToAllowed(maxPrice, OTM_PRICES, 'ceil')));
|
||||
if (selectedTypes.length > 0) {
|
||||
const otmTypes = [
|
||||
...new Set(selectedTypes.map((t) => PROPERTY_TYPE_MAP[t]?.onthemarket).filter(Boolean)),
|
||||
|
|
@ -227,20 +170,17 @@ export function buildPropertySearchUrls({
|
|||
}
|
||||
}
|
||||
otmParams.set('view', 'map-list');
|
||||
const otmPath = isRent ? 'to-rent' : 'for-sale';
|
||||
const onthemarket = `https://www.onthemarket.com/${otmPath}/property/${otmSlug}/?${otmParams.toString()}`;
|
||||
const onthemarket = `https://www.onthemarket.com/for-sale/property/${otmSlug}/?${otmParams.toString()}`;
|
||||
|
||||
// Zoopla
|
||||
const zPrices = isRent ? ZOOPLA_RENTS : ZOOPLA_PRICES;
|
||||
const zParams = new URLSearchParams();
|
||||
zParams.set('q', postcode);
|
||||
const zSearchSource = isRent ? 'to-rent' : 'for-sale';
|
||||
zParams.set('search_source', zSearchSource);
|
||||
zParams.set('search_source', 'for-sale');
|
||||
zParams.set('radius', String(nearestRadius(radiusMiles, ZOOPLA_RADII)));
|
||||
if (minPrice !== undefined)
|
||||
zParams.set('price_min', String(snapToAllowed(minPrice, zPrices, 'floor')));
|
||||
zParams.set('price_min', String(snapToAllowed(minPrice, ZOOPLA_PRICES, 'floor')));
|
||||
if (maxPrice !== undefined)
|
||||
zParams.set('price_max', String(snapToAllowed(maxPrice, zPrices, 'ceil')));
|
||||
zParams.set('price_max', String(snapToAllowed(maxPrice, ZOOPLA_PRICES, 'ceil')));
|
||||
if (selectedTypes.length > 0) {
|
||||
const zTypes = [
|
||||
...new Set(selectedTypes.map((t) => PROPERTY_TYPE_MAP[t]?.zoopla).filter(Boolean)),
|
||||
|
|
@ -249,28 +189,7 @@ export function buildPropertySearchUrls({
|
|||
zParams.append('property_sub_type', zt!);
|
||||
}
|
||||
}
|
||||
const zoopla = `https://www.zoopla.co.uk/${zSearchSource}/property/?${zParams.toString()}`;
|
||||
const zoopla = `https://www.zoopla.co.uk/for-sale/property/?${zParams.toString()}`;
|
||||
|
||||
// OpenRent — rent mode only
|
||||
let openrent: string | null = null;
|
||||
if (isRent) {
|
||||
const postcodeNoSpaces = postcode.replace(/\s+/g, '');
|
||||
const orSlug = postcodeNoSpaces.toLowerCase();
|
||||
const orParams = new URLSearchParams();
|
||||
orParams.set('term', postcodeNoSpaces.toUpperCase());
|
||||
const radiusKm = Math.round((isPostcode ? 0.25 : radiusMiles) * 1.609);
|
||||
orParams.set('area', String(Math.max(1, radiusKm)));
|
||||
const rentFilter = filters['Asking rent (monthly)'];
|
||||
const minRent =
|
||||
Array.isArray(rentFilter) && typeof rentFilter[0] === 'number' ? rentFilter[0] : undefined;
|
||||
const maxRent =
|
||||
Array.isArray(rentFilter) && typeof rentFilter[1] === 'number' ? rentFilter[1] : undefined;
|
||||
if (minRent !== undefined) orParams.set('prices_min', String(Math.round(minRent)));
|
||||
if (maxRent !== undefined) orParams.set('prices_max', String(Math.round(maxRent)));
|
||||
if (minBedrooms !== undefined) orParams.set('bedrooms_min', String(Math.floor(minBedrooms)));
|
||||
if (maxBedrooms !== undefined) orParams.set('bedrooms_max', String(Math.ceil(maxBedrooms)));
|
||||
openrent = `https://www.openrent.co.uk/properties-to-rent/${orSlug}?${orParams.toString()}`;
|
||||
}
|
||||
|
||||
return { rightmove, onthemarket, zoopla, openrent };
|
||||
return { rightmove, onthemarket, zoopla };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,49 +74,6 @@ const FEATURE_ICON_PATHS: Record<string, ReactNode> = {
|
|||
<path d="M14.7 6.3a1 1 0 000 1.4l1.6 1.6a1 1 0 001.4 0l3.77-3.77a6 6 0 01-7.94 7.94l-6.91 6.91a2.12 2.12 0 01-3-3l6.91-6.91a6 6 0 017.94-7.94l-3.76 3.76z" />
|
||||
</>
|
||||
),
|
||||
'Asking price': (
|
||||
<>
|
||||
<path d="M20.59 13.41l-7.17 7.17a2 2 0 01-2.83 0L2 12V2h10l8.59 8.59a2 2 0 010 2.82z" />
|
||||
<line x1="7" y1="7" x2="7.01" y2="7" />
|
||||
</>
|
||||
),
|
||||
'Asking rent (monthly)': (
|
||||
<>
|
||||
<circle cx="9" cy="9" r="7" />
|
||||
<path d="M15.58 8.42A7 7 0 0122 15a7 7 0 01-7 7 7 7 0 01-6.58-4.58" />
|
||||
</>
|
||||
),
|
||||
Bedrooms: (
|
||||
<>
|
||||
<path d="M2 4v16" />
|
||||
<path d="M2 8h18a2 2 0 012 2v10" />
|
||||
<path d="M2 17h20" />
|
||||
<path d="M6 4v4" />
|
||||
</>
|
||||
),
|
||||
Bathrooms: (
|
||||
<>
|
||||
<path d="M4 12h16a1 1 0 011 1v3a4 4 0 01-4 4H7a4 4 0 01-4-4v-3a1 1 0 011-1z" />
|
||||
<path d="M6 12V5a2 2 0 012-2h3" />
|
||||
<line x1="14" y1="4" x2="17" y2="4" />
|
||||
</>
|
||||
),
|
||||
'Listing date': (
|
||||
<>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<polyline points="12 6 12 12 16 14" />
|
||||
</>
|
||||
),
|
||||
'Listing status': (
|
||||
<>
|
||||
<line x1="8" y1="6" x2="21" y2="6" />
|
||||
<line x1="8" y1="12" x2="21" y2="12" />
|
||||
<line x1="8" y1="18" x2="21" y2="18" />
|
||||
<line x1="3" y1="6" x2="3.01" y2="6" />
|
||||
<line x1="3" y1="12" x2="3.01" y2="12" />
|
||||
<line x1="3" y1="18" x2="3.01" y2="18" />
|
||||
</>
|
||||
),
|
||||
'Leasehold/Freehold': (
|
||||
<>
|
||||
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
|
||||
|
|
@ -424,7 +381,7 @@ const FEATURE_ICON_PATHS: Record<string, ReactNode> = {
|
|||
<path d="M1 1h4l2.68 13.39a2 2 0 002 1.61h9.72a2 2 0 002-1.61L23 6H6" />
|
||||
</>
|
||||
),
|
||||
'Number of parks within 2km': (
|
||||
'Number of parks within 1km': (
|
||||
<>
|
||||
<path d="M12 22v-7" />
|
||||
<path d="M17 15H7l2-4H5l7-9 7 9h-4l2 4z" />
|
||||
|
|
|
|||
|
|
@ -195,9 +195,12 @@ export function emojiToTwemojiUrl(emoji: string): string {
|
|||
}
|
||||
|
||||
/** Look up a discrete color from the enum palette by index (wraps if > palette size). */
|
||||
export function enumIndexToColor(index: number): [number, number, number] {
|
||||
const i = Math.round(Math.max(0, index)) % ENUM_PALETTE.length;
|
||||
return ENUM_PALETTE[i];
|
||||
export function enumIndexToColor(
|
||||
index: number,
|
||||
palette: [number, number, number][] = ENUM_PALETTE
|
||||
): [number, number, number] {
|
||||
const i = Math.round(Math.max(0, index)) % palette.length;
|
||||
return palette[i];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -216,7 +219,8 @@ export function getFeatureFillColor(
|
|||
densityGradient: GradientStop[],
|
||||
isDark: boolean,
|
||||
alpha: number,
|
||||
enumCount: number = 0
|
||||
enumCount: number = 0,
|
||||
enumPalette?: [number, number, number][]
|
||||
): [number, number, number, number] {
|
||||
if (colorRange) {
|
||||
if (value == null)
|
||||
|
|
@ -234,7 +238,7 @@ export function getFeatureFillColor(
|
|||
|
||||
// Discrete coloring for enum features (used as base; PieHexExtension overrides when active)
|
||||
if (enumCount > 0) {
|
||||
const rgb = enumIndexToColor(Math.round(value as number));
|
||||
const rgb = enumIndexToColor(Math.round(value as number), enumPalette);
|
||||
return [...rgb, alpha] as [number, number, number, number];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ export function summarizeParams(queryString: string): string {
|
|||
const colonIdx = entry.indexOf(':');
|
||||
return colonIdx > 0 ? entry.substring(0, colonIdx) : entry;
|
||||
})
|
||||
.filter((n) => n && n !== 'Listing status');
|
||||
.filter((n) => n);
|
||||
if (filterNames.length > 0) {
|
||||
parts.push(
|
||||
filterNames.length <= 2
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue