Update chars

This commit is contained in:
Andras Schmelczer 2026-02-07 10:00:13 +00:00
parent 609dd5278c
commit 46585a4b2b
8 changed files with 522 additions and 285 deletions

View file

@ -3,10 +3,11 @@ 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 { STACKED_GROUPS } from '../lib/consts';
import { DualHistogram, LoadingSkeleton } from './DualHistogram';
import EnumBarChart from './EnumBarChart';
import StackedBarChart from './StackedBarChart';
import PriceHistoryChart from './PriceHistoryChart';
import ExternalSearchLinks from './ExternalSearchLinks';
import { InfoIcon, CloseIcon } from './ui/Icons';
import { IconButton } from './ui/IconButton';
@ -104,17 +105,19 @@ export default function AreaPane({
<LoadingSkeleton />
) : stats ? (
<div className="p-3 space-y-4">
{stats.price_history && stats.price_history.length > 0 && (
<div className="bg-warm-50 dark:bg-warm-800 rounded p-2">
<span className="text-xs text-warm-700 dark:text-warm-300">Price History</span>
<PriceHistoryChart points={stats.price_history} />
</div>
)}
{featureGroups.map((group) => {
const hasData = group.features.some(
(feature) => numericByName.has(feature.name) || enumByName.has(feature.name)
);
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;
const stackedCharts = STACKED_GROUPS[group.name];
return (
<div key={group.name}>
@ -122,38 +125,43 @@ export default function AreaPane({
{group.name}
</h3>
<div className="space-y-3">
{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,
};
})
{stackedCharts
? // Render stacked charts for this group
stackedCharts.map((chart) => {
const segments = chart.components
.map((name) => ({
name,
value: numericByName.get(name)?.mean ?? 0,
}))
.filter((s) => s.value > 0);
// Use aggregate feature stats if available, otherwise sum components
const aggregateStats = chart.feature
? numericByName.get(chart.feature)
: undefined;
const total = aggregateStats
? aggregateStats.mean
: segments.reduce((sum, s) => sum + s.value, 0);
const featureMeta = chart.feature
? globalFeatureByName.get(chart.feature)
: undefined;
if (total === 0) return null;
return (
<div
key={feature.name}
key={chart.label}
className="bg-warm-50 dark:bg-warm-800 rounded p-2"
>
<div className="flex justify-between items-baseline mb-1.5">
<div className="flex items-center gap-1 min-w-0 mr-2">
<span className="text-xs text-warm-700 dark:text-warm-300 truncate">
{feature.name}
{chart.label}
</span>
{feature.detail && (
{featureMeta?.detail && (
<button
onClick={() => setInfoFeature(feature)}
onClick={() => setInfoFeature(featureMeta)}
className="text-warm-400 hover:text-warm-700 dark:hover:text-warm-300 shrink-0"
title="Feature info"
>
@ -162,96 +170,105 @@ export default function AreaPane({
)}
</div>
<span className="text-xs font-semibold text-teal-700 dark:text-teal-400 whitespace-nowrap">
{formatValue(numericStats.mean)} avg/yr
{formatValue(total)}
{chart.unit ? ` ${chart.unit}` : ''}
</span>
</div>
<StackedBarChart segments={segments} total={numericStats.mean} />
<StackedBarChart segments={segments} total={total} />
</div>
);
}
})
: // Default: render each feature individually
group.features.map((feature) => {
const numericStats = numericByName.get(feature.name);
const enumStats = enumByName.get(feature.name);
// Regular numeric feature with histogram
const globalFeature = globalFeatureByName.get(feature.name);
const globalHistogram = globalFeature?.histogram;
const globalMean = globalHistogram
? calculateHistogramMean(globalHistogram)
: undefined;
if (numericStats) {
const globalFeature = globalFeatureByName.get(feature.name);
const globalHistogram = globalFeature?.histogram;
const globalMean = globalHistogram
? calculateHistogramMean(globalHistogram)
: undefined;
return (
<div
key={feature.name}
className="bg-warm-50 dark:bg-warm-800 rounded p-2"
>
<div className="flex justify-between items-baseline">
<div className="flex items-center gap-1 min-w-0 mr-2">
<span className="text-xs text-warm-700 dark:text-warm-300 truncate">
{feature.name}
</span>
{feature.detail && (
<button
onClick={() => setInfoFeature(feature)}
className="text-warm-400 hover:text-warm-700 dark:hover:text-warm-300 shrink-0"
title="Feature info"
>
<InfoIcon className="w-3 h-3" />
</button>
return (
<div
key={feature.name}
className="bg-warm-50 dark:bg-warm-800 rounded p-2"
>
<div className="flex justify-between items-baseline">
<div className="flex items-center gap-1 min-w-0 mr-2">
<span className="text-xs text-warm-700 dark:text-warm-300 truncate">
{feature.name}
</span>
{feature.detail && (
<button
onClick={() => setInfoFeature(feature)}
className="text-warm-400 hover:text-warm-700 dark:hover:text-warm-300 shrink-0"
title="Feature info"
>
<InfoIcon className="w-3 h-3" />
</button>
)}
</div>
<span className="text-xs font-semibold text-teal-700 dark:text-teal-400 whitespace-nowrap">
{formatValue(numericStats.mean)}
</span>
</div>
{numericStats.histogram && (
<>
<div className="flex justify-between text-[10px] text-warm-400 dark:text-warm-500 mt-0.5">
<span>{formatValue(numericStats.histogram.min)}</span>
<span>{formatValue(numericStats.histogram.max)}</span>
</div>
{globalHistogram ? (
<DualHistogram
localCounts={numericStats.histogram.counts}
globalCounts={globalHistogram.counts}
min={numericStats.histogram.min}
max={numericStats.histogram.max}
globalMean={globalMean}
/>
) : (
<DualHistogram
localCounts={numericStats.histogram.counts}
globalCounts={numericStats.histogram.counts}
min={numericStats.histogram.min}
max={numericStats.histogram.max}
/>
)}
</>
)}
</div>
<span className="text-xs font-semibold text-teal-700 dark:text-teal-400 whitespace-nowrap">
{formatValue(numericStats.mean)}
</span>
</div>
<div className="flex justify-between text-[10px] text-warm-400 dark:text-warm-500 mt-0.5">
<span>{formatValue(numericStats.histogram.min)}</span>
<span>{formatValue(numericStats.histogram.max)}</span>
</div>
{globalHistogram ? (
<DualHistogram
localCounts={numericStats.histogram.counts}
globalCounts={globalHistogram.counts}
min={numericStats.histogram.min}
max={numericStats.histogram.max}
globalMean={globalMean}
/>
) : (
<DualHistogram
localCounts={numericStats.histogram.counts}
globalCounts={numericStats.histogram.counts}
min={numericStats.histogram.min}
max={numericStats.histogram.max}
/>
)}
</div>
);
}
);
}
if (enumStats) {
return (
<div
key={feature.name}
className="bg-warm-50 dark:bg-warm-800 rounded p-2"
>
<div className="flex items-center gap-1">
<span className="text-xs text-warm-700 dark:text-warm-300">
{feature.name}
</span>
{feature.detail && (
<button
onClick={() => setInfoFeature(feature)}
className="text-warm-400 hover:text-warm-700 dark:hover:text-warm-300"
title="Feature info"
>
<InfoIcon className="w-3 h-3" />
</button>
)}
</div>
<EnumBarChart counts={enumStats.counts} />
</div>
);
}
if (enumStats) {
return (
<div
key={feature.name}
className="bg-warm-50 dark:bg-warm-800 rounded p-2"
>
<div className="flex items-center gap-1">
<span className="text-xs text-warm-700 dark:text-warm-300">
{feature.name}
</span>
{feature.detail && (
<button
onClick={() => setInfoFeature(feature)}
className="text-warm-400 hover:text-warm-700 dark:hover:text-warm-300"
title="Feature info"
>
<InfoIcon className="w-3 h-3" />
</button>
)}
</div>
<EnumBarChart counts={enumStats.counts} />
</div>
);
}
return null;
})}
return null;
})}
</div>
</div>
);