Lots of frontend changes

This commit is contained in:
Andras Schmelczer 2026-02-07 19:10:53 +00:00
parent ec29631c44
commit 555ba7cf53
38 changed files with 1508 additions and 648 deletions

View file

@ -1,10 +1,62 @@
export function formatValue(value: number): string {
if (Math.abs(value) >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
if (Math.abs(value) >= 1_000) return `${(value / 1_000).toFixed(1)}k`;
if (Number.isInteger(value)) return value.toLocaleString();
return value.toFixed(1);
export interface ValueFormat {
prefix?: string;
suffix?: string;
/** Show full integer (no k/M abbreviation) */
raw?: boolean;
}
export function formatValue(value: number, fmt?: ValueFormat): string {
const p = fmt?.prefix ?? '';
const s = fmt?.suffix ?? '';
if (fmt?.raw) return `${p}${Math.round(value)}${s}`;
if (Math.abs(value) >= 1_000_000) return `${p}${(value / 1_000_000).toFixed(1)}M${s}`;
if (Math.abs(value) >= 1_000) return `${p}${(value / 1_000).toFixed(1)}k${s}`;
if (Number.isInteger(value)) return `${p}${value.toLocaleString()}${s}`;
return `${p}${value.toFixed(1)}${s}`;
}
/** Lookup table for feature-specific formatting */
export const FEATURE_FORMATS: Record<string, ValueFormat> = {
// Property
'Last known price': { prefix: '£' },
'Price per sqm': { prefix: '£' },
'Total floor area (sqm)': { suffix: ' sqm' },
'Number of bedrooms & living rooms': { suffix: ' rooms' },
'Transaction year': { raw: true },
'Construction age': { raw: true },
// Transport
'Public transport to Bank (mins)': { suffix: ' mins' },
'Public transport to Fitzrovia (mins)': { suffix: ' mins' },
'Cycling to Bank (mins)': { suffix: ' mins' },
'Cycling to Fitzrovia (mins)': { suffix: ' mins' },
// Crime
'Anti-social behaviour (avg/yr)': { suffix: '/yr' },
'Violence and sexual offences (avg/yr)': { suffix: '/yr' },
'Criminal damage and arson (avg/yr)': { suffix: '/yr' },
'Burglary (avg/yr)': { suffix: '/yr' },
'Vehicle crime (avg/yr)': { suffix: '/yr' },
'Robbery (avg/yr)': { suffix: '/yr' },
'Other theft (avg/yr)': { suffix: '/yr' },
'Shoplifting (avg/yr)': { suffix: '/yr' },
'Drugs (avg/yr)': { suffix: '/yr' },
'Possession of weapons (avg/yr)': { suffix: '/yr' },
'Public order (avg/yr)': { suffix: '/yr' },
'Bicycle theft (avg/yr)': { suffix: '/yr' },
'Theft from the person (avg/yr)': { suffix: '/yr' },
'Other crime (avg/yr)': { suffix: '/yr' },
'Serious crime (avg/yr)': { suffix: '/yr' },
'Minor crime (avg/yr)': { suffix: '/yr' },
// Demographics
'% White': { suffix: '%' },
'% Asian': { suffix: '%' },
'% Black': { suffix: '%' },
'% Mixed': { suffix: '%' },
'% Other': { suffix: '%' },
// Environment
'Noise (dB)': { suffix: ' dB' },
'Max available download speed (Mbps)': { suffix: ' Mbps', raw: true },
};
export function formatFilterValue(value: number): string {
if (Math.abs(value) >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
if (Math.abs(value) >= 1_000) return `${(value / 1_000).toFixed(1)}k`;
@ -29,20 +81,31 @@ export function formatNumber(value: number | undefined, decimals = 0): string {
return decimals > 0 ? value.toFixed(decimals) : Math.round(value).toLocaleString();
}
// Calculate weighted mean from histogram
// Calculate weighted mean from histogram with outlier bins.
// Bin 0 = [min, p1), bins 1..n-2 = [p1, p99) evenly, bin n-1 = [p99, max].
export function calculateHistogramMean(histogram: {
min: number;
bin_width: number;
max: number;
p1: number;
p99: number;
counts: number[];
}): number | undefined {
if (!histogram.counts.length) return undefined;
const n = histogram.counts.length;
if (n === 0) return undefined;
const totalCount = histogram.counts.reduce((a, b) => a + b, 0);
if (totalCount === 0) return undefined;
const { min, max, p1, p99 } = histogram;
const middleBins = Math.max(n - 2, 0);
const middleWidth = middleBins > 0 && p99 > p1 ? (p99 - p1) / middleBins : 0;
let weightedSum = 0;
for (let i = 0; i < histogram.counts.length; i++) {
const binCenter = histogram.min + (i + 0.5) * histogram.bin_width;
weightedSum += binCenter * histogram.counts[i];
for (let i = 0; i < n; i++) {
let center: number;
if (i === 0) center = (min + p1) / 2;
else if (i === n - 1) center = (p99 + max) / 2;
else center = p1 + (i - 0.5) * middleWidth;
weightedSum += center * histogram.counts[i];
}
return weightedSum / totalCount;
}