diff --git a/frontend/src/components/AreaPane.tsx b/frontend/src/components/AreaPane.tsx index 44ed8d0..b1adc5e 100644 --- a/frontend/src/components/AreaPane.tsx +++ b/frontend/src/components/AreaPane.tsx @@ -3,8 +3,10 @@ import type { FeatureFilters, FeatureMeta, HexagonStatsResponse, PostcodeData } import type { HexagonLocation } from '../lib/external-search'; import { formatValue, calculateHistogramMean } from '../lib/format'; import { groupFeaturesByCategory } from '../lib/features'; +import { CRIME_BREAKDOWNS } from '../lib/consts'; import { DualHistogram, LoadingSkeleton } from './DualHistogram'; import EnumBarChart from './EnumBarChart'; +import StackedBarChart from './StackedBarChart'; import ExternalSearchLinks from './ExternalSearchLinks'; import { InfoIcon, CloseIcon } from './ui/Icons'; import { IconButton } from './ui/IconButton'; @@ -108,17 +110,67 @@ export default function AreaPane({ ); if (!hasData) return null; + // For Crime group, only show aggregate features with stacked breakdown + const isCrimeGroup = group.name === 'Crime'; + const featuresToRender = isCrimeGroup + ? group.features.filter((f) => f.name in CRIME_BREAKDOWNS) + : group.features; + return (

{group.name}

- {group.features.map((feature) => { + {featuresToRender.map((feature) => { const numericStats = numericByName.get(feature.name); const enumStats = enumByName.get(feature.name); if (numericStats) { + // Check if this is a crime aggregate that should show breakdown + const breakdown = CRIME_BREAKDOWNS[feature.name]; + if (breakdown) { + // Build segments from component crime means + const segments = breakdown + .map((componentName) => { + const componentStats = numericByName.get(componentName); + return { + name: componentName, + value: componentStats?.mean ?? 0, + }; + }) + .filter((s) => s.value > 0); + + return ( +
+
+
+ + {feature.name} + + {feature.detail && ( + + )} +
+ + {formatValue(numericStats.mean)} avg/yr + +
+ +
+ ); + } + + // Regular numeric feature with histogram const globalFeature = globalFeatureByName.get(feature.name); const globalHistogram = globalFeature?.histogram; const globalMean = globalHistogram diff --git a/frontend/src/components/Filters.tsx b/frontend/src/components/Filters.tsx index 547d265..1d5d7aa 100644 --- a/frontend/src/components/Filters.tsx +++ b/frontend/src/components/Filters.tsx @@ -1,10 +1,9 @@ import { memo, useState, useRef, useCallback, useMemo, useEffect } from 'react'; -import { Slider } from './ui/slider'; -import { Label } from './ui/label'; +import { Slider } from './ui/Slider'; +import { Label } from './ui/Label'; import { SearchInput } from './ui/SearchInput'; import { SelectionButtons } from './ui/SelectionButtons'; -import { ChevronIcon, FilterIcon, LightbulbIcon } from './ui/Icons'; -import { IconButton } from './ui/IconButton'; +import { FilterIcon, LightbulbIcon } from './ui/Icons'; import { EmptyState } from './ui/EmptyState'; import type { FeatureMeta, FeatureFilters } from '../types'; import { formatFilterValue } from '../lib/format'; @@ -34,7 +33,6 @@ interface FiltersProps { onNavigateToSource?: (slug: string, featureName: string) => void; openInfoFeature?: string | null; onClearOpenInfoFeature?: () => void; - onCollapse?: () => void; } function FeatureBrowser({ @@ -152,7 +150,6 @@ export default memo(function Filters({ onNavigateToSource, openInfoFeature, onClearOpenInfoFeature, - onCollapse, }: FiltersProps) { const availableFeatures = features.filter((f) => !enabledFeatures.has(f.name)); const enabledFeatureList = features.filter((f) => enabledFeatures.has(f.name)); @@ -187,11 +184,6 @@ export default memo(function Filters({ className="flex flex-col bg-white dark:bg-navy-950 overflow-hidden h-full" >
- {onCollapse && ( - - - - )}