Lots of frontend changes
This commit is contained in:
parent
ec29631c44
commit
555ba7cf53
38 changed files with 1508 additions and 648 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue