This commit is contained in:
Andras Schmelczer 2026-05-09 09:26:40 +01:00
parent 701c17a703
commit f114ada255
44 changed files with 5264 additions and 1674 deletions

View file

@ -16,7 +16,12 @@ import {
roundedPercentages,
} from '../../lib/format';
import { groupFeaturesByCategory } from '../../lib/features';
import { PARTY_FEATURE_COLORS, STACKED_GROUPS, STACKED_ENUM_GROUPS } from '../../lib/consts';
import {
PARTY_FEATURE_COLORS,
STACKED_GROUPS,
STACKED_ENUM_GROUPS,
STACKED_SEGMENT_COLORS,
} from '../../lib/consts';
import { DualHistogram, LoadingSkeleton } from './DualHistogram';
import EnumBarChart from './EnumBarChart';
import StackedBarChart from './StackedBarChart';
@ -113,71 +118,75 @@ export default function AreaPane({
return (
<>
<div className="h-full overflow-y-auto">
<div className="p-3">
<div className="flex items-center gap-2">
<div>
<h2 className="text-sm font-semibold dark:text-warm-100">
{isPostcode ? hexagonId : t('areaPane.areaStatistics')}
</h2>
{isPostcode && (
<span className="text-xs text-warm-500 dark:text-warm-400">
{t('common.postcode')}
</span>
)}
<div className="border-b border-warm-200 bg-white dark:border-navy-700 dark:bg-navy-950">
<div className="space-y-3 p-3">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="flex min-w-0 items-center gap-2">
<h2 className="truncate text-base font-semibold text-warm-900 dark:text-warm-100">
{isPostcode ? hexagonId : t('areaPane.areaOverview')}
</h2>
{loading && (
<span className="h-3 w-3 shrink-0 rounded-full border-2 border-teal-600 border-t-transparent dark:border-teal-400 dark:border-t-transparent animate-spin" />
)}
</div>
<p className="mt-0.5 text-xs leading-snug text-warm-500 dark:text-warm-400">
{t('areaPane.statsFor', {
type: isPostcode
? t('common.postcode').toLowerCase()
: t('common.area').toLowerCase(),
})}
</p>
</div>
<div className="shrink-0 text-right">
<div className="text-lg font-semibold tabular-nums leading-none text-navy-950 dark:text-warm-50">
{propertyCount == null ? '...' : propertyCount.toLocaleString()}
</div>
<div className="mt-0.5 text-xs font-medium text-warm-500 dark:text-warm-400">
{t('common.propertiesPlural')}
</div>
</div>
</div>
{loading && stats && (
<div className="w-3 h-3 border-2 border-teal-600 dark:border-teal-400 border-t-transparent rounded-full animate-spin" />
<div className="flex gap-2 border-l-2 border-teal-500 bg-warm-50 px-2.5 py-2 text-xs leading-snug text-warm-700 dark:bg-navy-900 dark:text-warm-300">
<FilterIcon className="mt-0.5 h-3.5 w-3.5 shrink-0 text-teal-700 dark:text-teal-300" />
<p>
{activeFilterCount > 0
? t('areaPane.filtersAffectStats', { count: activeFilterCount })
: t('areaPane.noFiltersAffectStats')}
</p>
</div>
{hasFilteredOutArea && (
<div className="mt-2 rounded border border-amber-200 bg-amber-50 px-2.5 py-2 text-xs leading-snug text-amber-900 dark:border-amber-800/70 dark:bg-amber-950/40 dark:text-amber-100">
<p className="font-semibold">{t('areaPane.noFilteredMatches')}</p>
<p className="mt-1">
{unfilteredCount != null && unfilteredCount > 0
? t('areaPane.unfilteredAreaCount', { count: unfilteredCount })
: unfilteredCount === 0
? t('areaPane.noUnfilteredAreaProperties')
: t('areaPane.relaxFiltersHint')}
</p>
{onClearFilters && (
<button
type="button"
onClick={onClearFilters}
className="mt-2 rounded bg-amber-600 px-2 py-1 text-xs font-medium text-white hover:bg-amber-700 dark:bg-amber-500 dark:text-amber-950 dark:hover:bg-amber-400"
>
{t('filters.clearAll')}
</button>
)}
</div>
)}
{stats && stats.count > 0 && (
<button
onClick={onViewProperties}
className="w-full text-sm py-1.5 rounded bg-teal-600 hover:bg-teal-700 text-white font-medium"
>
{t('areaPane.viewPropertiesShort')}
</button>
)}
</div>
{propertyCount != null && (
<p className="text-sm text-warm-600 dark:text-warm-400 mt-1">
{propertyCount.toLocaleString()} {t('common.propertiesPlural')}
</p>
)}
<p className="text-xs text-warm-500 dark:text-warm-400 mt-1">
{t('areaPane.statsFor', {
type: isPostcode
? t('common.postcode').toLowerCase()
: t('common.area').toLowerCase(),
})}
</p>
<div className="mt-2 flex gap-2 rounded border border-teal-200 bg-teal-50 px-2.5 py-2 text-xs leading-snug text-teal-800 dark:border-teal-800/70 dark:bg-teal-950/40 dark:text-teal-200">
<FilterIcon className="mt-0.5 h-3.5 w-3.5 shrink-0" />
<p>
{activeFilterCount > 0
? t('areaPane.filtersAffectStats', { count: activeFilterCount })
: t('areaPane.noFiltersAffectStats')}
</p>
</div>
{hasFilteredOutArea && (
<div className="mt-2 rounded border border-amber-200 bg-amber-50 px-2.5 py-2 text-xs leading-snug text-amber-900 dark:border-amber-800/70 dark:bg-amber-950/40 dark:text-amber-100">
<p className="font-semibold">{t('areaPane.noFilteredMatches')}</p>
<p className="mt-1">
{unfilteredCount != null && unfilteredCount > 0
? t('areaPane.unfilteredAreaCount', { count: unfilteredCount })
: unfilteredCount === 0
? t('areaPane.noUnfilteredAreaProperties')
: t('areaPane.relaxFiltersHint')}
</p>
{onClearFilters && (
<button
type="button"
onClick={onClearFilters}
className="mt-2 rounded bg-amber-600 px-2 py-1 text-xs font-medium text-white hover:bg-amber-700 dark:bg-amber-500 dark:text-amber-950 dark:hover:bg-amber-400"
>
{t('filters.clearAll')}
</button>
)}
</div>
)}
{stats && stats.count > 0 && (
<button
onClick={onViewProperties}
className="mt-2 w-full text-sm py-1.5 rounded bg-teal-600 hover:bg-teal-700 text-white font-medium"
>
{t('areaPane.viewProperties', { count: stats.count })}
</button>
)}
</div>
{hexagonLocation && stats && (
@ -315,7 +324,7 @@ export default function AreaPane({
colorMap={
chart.label === 'Political vote share'
? PARTY_FEATURE_COLORS
: undefined
: STACKED_SEGMENT_COLORS
}
/>
</div>
@ -369,7 +378,15 @@ export default function AreaPane({
p1={numericStats.histogram.p1}
p99={numericStats.histogram.p99}
globalMean={globalMean}
formatLabel={(v) => formatFilterValue(v, feature.raw)}
meanLabel={t('areaPane.nationalAvg')}
formatLabel={(v) =>
formatFilterValue(
v,
feature.suffix === '%'
? { raw: feature.raw, suffix: feature.suffix }
: feature.raw
)
}
/>
) : (
<DualHistogram
@ -377,7 +394,14 @@ export default function AreaPane({
globalCounts={numericStats.histogram.counts}
p1={numericStats.histogram.p1}
p99={numericStats.histogram.p99}
formatLabel={(v) => formatFilterValue(v, feature.raw)}
formatLabel={(v) =>
formatFilterValue(
v,
feature.suffix === '%'
? { raw: feature.raw, suffix: feature.suffix }
: feature.raw
)
}
/>
))}
</div>