seems alright

This commit is contained in:
Andras Schmelczer 2026-05-17 13:52:11 +01:00
parent ebe7bbb51d
commit eac1bd0d13
58 changed files with 23125 additions and 153505 deletions

View file

@ -131,7 +131,7 @@ export default function AreaPane({
};
const formatExclusionValue = (exclusion: FilterExclusion, value: number) => {
if (exclusion.kind === 'travel') return `${Math.round(value)} ${t('common.min')}`;
if (exclusion.kind === 'travel') return `${Math.round(value)} ${t('common.minute')}`;
return formatFilterValue(value, filterValueFormat(globalFeatureByName.get(exclusion.name)));
};
@ -165,369 +165,217 @@ export default function AreaPane({
<div className="relative flex h-full flex-col">
<IndeterminateProgressBar show={loading && stats != null} />
<div className="flex-1 overflow-y-auto">
<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 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>
<div className="rounded border border-warm-200 bg-warm-50 px-2.5 py-2 dark:border-navy-700 dark:bg-navy-900">
<div className="flex items-center justify-between gap-2">
<span className="text-xs font-semibold text-warm-700 dark:text-warm-200">
{t('areaPane.statsBasis')}
</span>
<div className="inline-flex shrink-0 rounded-md bg-warm-200 p-0.5 dark:bg-navy-800">
<button
type="button"
disabled={!filtersActive}
aria-pressed={statsUseFilters && filtersActive}
onClick={() => onStatsUseFiltersChange(true)}
className={`rounded px-2 py-1 text-xs font-medium ${
statsUseFilters && filtersActive
? 'bg-white text-teal-700 shadow-sm dark:bg-navy-700 dark:text-teal-300'
: 'text-warm-600 hover:text-warm-900 disabled:cursor-not-allowed disabled:opacity-50 dark:text-warm-400 dark:hover:text-warm-100'
}`}
>
{t('areaPane.matchingFiltersOption')}
</button>
<button
type="button"
aria-pressed={!statsUseFilters || !filtersActive}
onClick={() => onStatsUseFiltersChange(false)}
className={`rounded px-2 py-1 text-xs font-medium ${
!statsUseFilters || !filtersActive
? 'bg-white text-teal-700 shadow-sm dark:bg-navy-700 dark:text-teal-300'
: 'text-warm-600 hover:text-warm-900 dark:text-warm-400 dark:hover:text-warm-100'
}`}
>
{t('areaPane.allPropertiesOption')}
</button>
</div>
</div>
<p className="mt-1.5 text-xs leading-snug text-warm-500 dark:text-warm-400">
{filtersActive
? statsUseFilters
? t('areaPane.filtersAffectStats', { count: activeFilterCount })
: t('areaPane.filtersIgnoredForStats')
: t('areaPane.noFiltersAffectStats')}
</p>
</div>
{showFlipToggleCallout && (
<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.filteredStatsEmpty')}</p>
<p className="mt-1">
{unfilteredCount != null
? t('areaPane.showAllStatsHint', { count: unfilteredCount })
: t('areaPane.showAllStatsFallback')}
</p>
<button
type="button"
onClick={() => onStatsUseFiltersChange(false)}
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('areaPane.showAllStats')}
</button>
{filterExclusions.length > 0 && (
<div className="mt-2 border-t border-amber-200 pt-2 dark:border-amber-800/70">
<p className="font-semibold">{t('areaPane.closestBlockingFilters')}</p>
<ol className="mt-1.5 space-y-1.5">
{filterExclusions.map((exclusion) => (
<li
key={`${exclusion.kind}:${exclusion.name}:${exclusion.direction}:${exclusion.category ?? ''}`}
className="rounded bg-white/70 px-2 py-1.5 dark:bg-navy-950/40"
>
<div className="truncate font-medium">
{getExclusionLabel(exclusion)}
</div>
<p className="mt-0.5 text-amber-800/80 dark:text-amber-100/80">
{getExclusionAdjustment(exclusion)}
</p>
</li>
))}
</ol>
</div>
)}
</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>
</div>
<div className="rounded border border-warm-200 bg-warm-50 px-2.5 py-2 dark:border-navy-700 dark:bg-navy-900">
<div className="flex items-center justify-between gap-2">
<span className="text-xs font-semibold text-warm-700 dark:text-warm-200">
{t('areaPane.statsBasis')}
</span>
<div className="inline-flex shrink-0 rounded-md bg-warm-200 p-0.5 dark:bg-navy-800">
<button
type="button"
disabled={!filtersActive}
aria-pressed={statsUseFilters && filtersActive}
onClick={() => onStatsUseFiltersChange(true)}
className={`rounded px-2 py-1 text-xs font-medium ${
statsUseFilters && filtersActive
? 'bg-white text-teal-700 shadow-sm dark:bg-navy-700 dark:text-teal-300'
: 'text-warm-600 hover:text-warm-900 disabled:cursor-not-allowed disabled:opacity-50 dark:text-warm-400 dark:hover:text-warm-100'
}`}
>
{t('areaPane.matchingFiltersOption')}
</button>
<button
type="button"
aria-pressed={!statsUseFilters || !filtersActive}
onClick={() => onStatsUseFiltersChange(false)}
className={`rounded px-2 py-1 text-xs font-medium ${
!statsUseFilters || !filtersActive
? 'bg-white text-teal-700 shadow-sm dark:bg-navy-700 dark:text-teal-300'
: 'text-warm-600 hover:text-warm-900 dark:text-warm-400 dark:hover:text-warm-100'
}`}
>
{t('areaPane.allPropertiesOption')}
</button>
</div>
</div>
<p className="mt-1.5 text-xs leading-snug text-warm-500 dark:text-warm-400">
{filtersActive
? statsUseFilters
? t('areaPane.filtersAffectStats', { count: activeFilterCount })
: t('areaPane.filtersIgnoredForStats')
: t('areaPane.noFiltersAffectStats')}
</p>
</div>
{showFlipToggleCallout && (
<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.filteredStatsEmpty')}</p>
<p className="mt-1">
{unfilteredCount != null
? t('areaPane.showAllStatsHint', { count: unfilteredCount })
: t('areaPane.showAllStatsFallback')}
</p>
<button
type="button"
onClick={() => onStatsUseFiltersChange(false)}
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('areaPane.showAllStats')}
</button>
{filterExclusions.length > 0 && (
<div className="mt-2 border-t border-amber-200 pt-2 dark:border-amber-800/70">
<p className="font-semibold">{t('areaPane.closestBlockingFilters')}</p>
<ol className="mt-1.5 space-y-1.5">
{filterExclusions.map((exclusion) => (
<li
key={`${exclusion.kind}:${exclusion.name}:${exclusion.direction}:${exclusion.category ?? ''}`}
className="rounded bg-white/70 px-2 py-1.5 dark:bg-navy-950/40"
>
<div className="truncate font-medium">{getExclusionLabel(exclusion)}</div>
<p className="mt-0.5 text-amber-800/80 dark:text-amber-100/80">
{getExclusionAdjustment(exclusion)}
</p>
</li>
))}
</ol>
{hexagonLocation && stats && (
<ExternalSearchLinks location={hexagonLocation} filters={filters} />
)}
{(() => {
const journeyPostcode = isPostcode ? hexagonId : stats?.central_postcode;
return journeyPostcode && travelTimeEntries && travelTimeEntries.length > 0 ? (
<JourneyInstructions
postcode={journeyPostcode}
entries={travelTimeEntries}
label={!isPostcode ? journeyPostcode : undefined}
shareCode={shareCode}
/>
) : null;
})()}
{loading && !stats ? (
<LoadingSkeleton />
) : stats ? (
<div>
{hexagonLocation && <StreetViewEmbed location={hexagonLocation} />}
{stats.count > 0 && <HistogramLegend />}
{stats.price_history &&
(() => {
const uniqueYears = new Set(stats.price_history.map((p) => Math.floor(p.year)));
return uniqueYears.size > 1;
})() && (
<div className="mx-3 mt-2 bg-warm-50 dark:bg-warm-800 rounded p-2">
<span className="text-xs text-warm-700 dark:text-warm-300">
{t('areaPane.priceHistory')}
</span>
<PriceHistoryChart points={stats.price_history} />
</div>
)}
</div>
)}
</div>
</div>
{featureGroups.map((group) => {
const hasData = group.features.some(
(feature) => numericByName.has(feature.name) || enumByName.has(feature.name)
);
if (!hasData) return null;
{hexagonLocation && stats && (
<ExternalSearchLinks location={hexagonLocation} filters={filters} />
)}
{(() => {
const journeyPostcode = isPostcode ? hexagonId : stats?.central_postcode;
return journeyPostcode && travelTimeEntries && travelTimeEntries.length > 0 ? (
<JourneyInstructions
postcode={journeyPostcode}
entries={travelTimeEntries}
label={!isPostcode ? journeyPostcode : undefined}
shareCode={shareCode}
/>
) : null;
})()}
{loading && !stats ? (
<LoadingSkeleton />
) : stats ? (
<div>
{hexagonLocation && <StreetViewEmbed location={hexagonLocation} />}
{stats.count > 0 && <HistogramLegend />}
{stats.price_history &&
(() => {
const uniqueYears = new Set(stats.price_history.map((p) => Math.floor(p.year)));
return uniqueYears.size > 1;
})() && (
<div className="mx-3 mt-2 bg-warm-50 dark:bg-warm-800 rounded p-2">
<span className="text-xs text-warm-700 dark:text-warm-300">
{t('areaPane.priceHistory')}
</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;
const stackedCharts = STACKED_GROUPS[group.name];
const stackedEnumCharts = STACKED_ENUM_GROUPS[group.name];
const stackedCharts = STACKED_GROUPS[group.name];
const stackedEnumCharts = STACKED_ENUM_GROUPS[group.name];
const stackedEnumFeatureNames = new Set<string>(
stackedEnumCharts?.flatMap((c) =>
[c.feature, ...c.components].filter((s): s is string => Boolean(s))
) ?? []
);
const stackedEnumFeatureNames = new Set<string>(
stackedEnumCharts?.flatMap((c) =>
[c.feature, ...c.components].filter((s): s is string => Boolean(s))
) ?? []
);
const expanded = isGroupExpanded(group.name);
const expanded = isGroupExpanded(group.name);
return (
<div key={group.name}>
<CollapsibleGroupHeader
name={group.name}
expanded={expanded}
onToggle={() => onToggleGroup(group.name)}
className="px-3 py-2.5 text-sm font-bold text-warm-500 bg-warm-50 dark:bg-warm-900 dark:text-warm-400 sticky top-0 z-10 hover:bg-warm-100 dark:hover:bg-warm-800"
/>
{expanded && (
<div className="px-3 py-2 space-y-3">
{stackedCharts?.map((chart) => {
const segments = chart.components
.map((name) => ({
name,
value: numericByName.get(name)?.mean ?? 0,
}))
.filter((s) => s.value > 0);
const isPercentageComposition = chart.unit === '%' && !chart.feature;
const displaySegments = isPercentageComposition
? normalizePercentageSegments(segments)
: segments;
const aggregateStats = chart.feature
? numericByName.get(chart.feature)
: undefined;
const total = aggregateStats
? aggregateStats.mean
: displaySegments.reduce((sum, s) => sum + s.value, 0);
// Use rateFeature (e.g. per-1k) for display if available
const rateStats = chart.rateFeature
? numericByName.get(chart.rateFeature)
: undefined;
const displayValue = isPercentageComposition
? 100
: rateStats
? rateStats.mean
: total;
// Use rateFeature for info popup and national average when available
const infoFeatureName = chart.rateFeature ?? chart.feature;
const featureMeta = infoFeatureName
? globalFeatureByName.get(infoFeatureName)
: undefined;
const globalMean = featureMeta?.histogram
? calculateHistogramMean(featureMeta.histogram)
: undefined;
if (total === 0) return null;
return (
<div
key={ts(chart.label)}
className="bg-warm-50 dark:bg-warm-800 rounded p-2"
>
<div className="flex justify-between items-baseline mb-1.5">
{featureMeta ? (
<FeatureLabel
feature={{ ...featureMeta, name: ts(chart.label) }}
onShowInfo={setInfoFeature}
className="mr-2"
/>
) : (
<span className="text-xs text-warm-700 dark:text-warm-300 truncate mr-2">
{ts(chart.label)}
</span>
)}
<div className="text-right shrink-0">
<span className="text-xs font-semibold text-teal-700 dark:text-teal-400 whitespace-nowrap">
{formatValue(displayValue)}
{chart.unit ? ` ${chart.unit}` : ''}
</span>
{globalMean != null && (
<div className="text-[10px] text-warm-400 dark:text-warm-500 whitespace-nowrap">
{t('areaPane.nationalAvg')}: {formatValue(globalMean)}
</div>
)}
</div>
</div>
<StackedBarChart
segments={displaySegments}
total={total}
colorMap={
chart.label === 'Political vote share'
? PARTY_FEATURE_COLORS
: STACKED_SEGMENT_COLORS
}
/>
</div>
);
})}
{(() => {
const stackedFeatureNames = new Set<string>(
stackedCharts?.flatMap((c) =>
[c.feature, c.rateFeature, ...c.components].filter((s): s is string =>
Boolean(s)
)
) ?? []
);
return group.features
.filter(
(f) =>
!stackedFeatureNames.has(f.name) &&
!stackedEnumFeatureNames.has(f.name)
)
.map((feature) => {
const numericStats = numericByName.get(feature.name);
const enumStats = enumByName.get(feature.name);
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">
<FeatureLabel
feature={feature}
onShowInfo={setInfoFeature}
className="mr-2"
/>
<span className="text-xs font-semibold text-teal-700 dark:text-teal-400 whitespace-nowrap">
{formatValue(numericStats.mean, feature)}
</span>
</div>
{numericStats.histogram &&
(globalHistogram ? (
<DualHistogram
localCounts={numericStats.histogram.counts}
globalCounts={globalHistogram.counts}
p1={numericStats.histogram.p1}
p99={numericStats.histogram.p99}
globalMean={globalMean}
meanLabel={t('areaPane.nationalAvg')}
formatLabel={(v) =>
formatFilterValue(
v,
feature.suffix === '%'
? { raw: feature.raw, suffix: feature.suffix }
: feature.raw
)
}
/>
) : (
<DualHistogram
localCounts={numericStats.histogram.counts}
globalCounts={numericStats.histogram.counts}
p1={numericStats.histogram.p1}
p99={numericStats.histogram.p99}
formatLabel={(v) =>
formatFilterValue(
v,
feature.suffix === '%'
? { raw: feature.raw, suffix: feature.suffix }
: feature.raw
)
}
/>
))}
</div>
);
}
if (enumStats) {
const globalFeature = globalFeatureByName.get(feature.name);
return (
<div
key={feature.name}
className="bg-warm-50 dark:bg-warm-800 rounded p-2"
>
<FeatureLabel feature={feature} onShowInfo={setInfoFeature} />
<EnumBarChart
counts={enumStats.counts}
globalCounts={globalFeature?.counts}
featureName={feature.name}
/>
</div>
);
}
return null;
});
})()}
{stackedEnumCharts?.map((chart) => {
const featureMeta = chart.feature
? globalFeatureByName.get(chart.feature)
: undefined;
if (chart.components.length === 1) {
const stats = enumByName.get(chart.components[0]);
if (!stats) return null;
const segments = chart.valueOrder
.map((value) => ({ name: value, value: stats.counts[value] ?? 0 }))
return (
<div key={group.name}>
<CollapsibleGroupHeader
name={group.name}
expanded={expanded}
onToggle={() => onToggleGroup(group.name)}
className="px-3 py-2.5 text-sm font-bold text-warm-500 bg-warm-50 dark:bg-warm-900 dark:text-warm-400 sticky top-0 z-10 hover:bg-warm-100 dark:hover:bg-warm-800"
/>
{expanded && (
<div className="px-3 py-2 space-y-3">
{stackedCharts?.map((chart) => {
const segments = chart.components
.map((name) => ({
name,
value: numericByName.get(name)?.mean ?? 0,
}))
.filter((s) => s.value > 0);
const total = segments.reduce((sum, s) => sum + s.value, 0);
const isPercentageComposition = chart.unit === '%' && !chart.feature;
const displaySegments = isPercentageComposition
? normalizePercentageSegments(segments)
: segments;
const aggregateStats = chart.feature
? numericByName.get(chart.feature)
: undefined;
const total = aggregateStats
? aggregateStats.mean
: displaySegments.reduce((sum, s) => sum + s.value, 0);
// Use rateFeature (e.g. per-1k) for display if available
const rateStats = chart.rateFeature
? numericByName.get(chart.rateFeature)
: undefined;
const displayValue = isPercentageComposition
? 100
: rateStats
? rateStats.mean
: total;
// Use rateFeature for info popup and national average when available
const infoFeatureName = chart.rateFeature ?? chart.feature;
const featureMeta = infoFeatureName
? globalFeatureByName.get(infoFeatureName)
: undefined;
const globalMean = featureMeta?.histogram
? calculateHistogramMean(featureMeta.histogram)
: undefined;
if (total === 0) return null;
return (
@ -538,7 +386,7 @@ export default function AreaPane({
<div className="flex justify-between items-baseline mb-1.5">
{featureMeta ? (
<FeatureLabel
feature={featureMeta}
feature={{ ...featureMeta, name: ts(chart.label) }}
onShowInfo={setInfoFeature}
className="mr-2"
/>
@ -547,62 +395,216 @@ export default function AreaPane({
{ts(chart.label)}
</span>
)}
<span className="text-xs font-semibold text-teal-700 dark:text-teal-400 whitespace-nowrap">
{total.toLocaleString()}
</span>
<div className="text-right shrink-0">
<span className="text-xs font-semibold text-teal-700 dark:text-teal-400 whitespace-nowrap">
{formatValue(displayValue)}
{chart.unit ? ` ${chart.unit}` : ''}
</span>
{globalMean != null && (
<div className="text-[10px] text-warm-400 dark:text-warm-500 whitespace-nowrap">
{t('areaPane.nationalAvg')}: {formatValue(globalMean)}
</div>
)}
</div>
</div>
<StackedBarChart
segments={segments}
segments={displaySegments}
total={total}
colorMap={Object.fromEntries(
chart.valueOrder.map((v, i) => [v, chart.valueColors[i]])
)}
colorMap={
chart.label === 'Political vote share'
? PARTY_FEATURE_COLORS
: STACKED_SEGMENT_COLORS
}
/>
</div>
);
}
})}
{(() => {
const stackedFeatureNames = new Set<string>(
stackedCharts?.flatMap((c) =>
[c.feature, c.rateFeature, ...c.components].filter((s): s is string =>
Boolean(s)
)
) ?? []
);
return group.features
.filter(
(f) =>
!stackedFeatureNames.has(f.name) &&
!stackedEnumFeatureNames.has(f.name)
)
.map((feature) => {
const numericStats = numericByName.get(feature.name);
const enumStats = enumByName.get(feature.name);
const components = chart.components
.map((name) => {
const stats = enumByName.get(name);
return stats ? { label: name, stats } : null;
})
.filter((c): c is NonNullable<typeof c> => c !== null);
if (numericStats) {
const globalFeature = globalFeatureByName.get(feature.name);
const globalHistogram = globalFeature?.histogram;
const globalMean = globalHistogram
? calculateHistogramMean(globalHistogram)
: undefined;
if (components.length === 0) return null;
return (
<div
key={feature.name}
className="bg-warm-50 dark:bg-warm-800 rounded p-2"
>
<div className="flex justify-between items-baseline">
<FeatureLabel
feature={feature}
onShowInfo={setInfoFeature}
className="mr-2"
/>
<span className="text-xs font-semibold text-teal-700 dark:text-teal-400 whitespace-nowrap">
{formatValue(numericStats.mean, feature)}
</span>
</div>
{numericStats.histogram &&
(globalHistogram ? (
<DualHistogram
localCounts={numericStats.histogram.counts}
globalCounts={globalHistogram.counts}
p1={numericStats.histogram.p1}
p99={numericStats.histogram.p99}
globalMean={globalMean}
meanLabel={t('areaPane.nationalAvg')}
formatLabel={(v) =>
formatFilterValue(
v,
feature.suffix === '%'
? { raw: feature.raw, suffix: feature.suffix }
: feature.raw
)
}
/>
) : (
<DualHistogram
localCounts={numericStats.histogram.counts}
globalCounts={numericStats.histogram.counts}
p1={numericStats.histogram.p1}
p99={numericStats.histogram.p99}
formatLabel={(v) =>
formatFilterValue(
v,
feature.suffix === '%'
? { raw: feature.raw, suffix: feature.suffix }
: feature.raw
)
}
/>
))}
</div>
);
}
return (
<div
key={ts(chart.label)}
className="bg-warm-50 dark:bg-warm-800 rounded p-2"
>
<div className="mb-1.5">
{featureMeta ? (
<FeatureLabel
feature={{ ...featureMeta, name: ts(chart.label) }}
onShowInfo={setInfoFeature}
if (enumStats) {
const globalFeature = globalFeatureByName.get(feature.name);
return (
<div
key={feature.name}
className="bg-warm-50 dark:bg-warm-800 rounded p-2"
>
<FeatureLabel feature={feature} onShowInfo={setInfoFeature} />
<EnumBarChart
counts={enumStats.counts}
globalCounts={globalFeature?.counts}
featureName={feature.name}
/>
</div>
);
}
return null;
});
})()}
{stackedEnumCharts?.map((chart) => {
const featureMeta = chart.feature
? globalFeatureByName.get(chart.feature)
: undefined;
if (chart.components.length === 1) {
const stats = enumByName.get(chart.components[0]);
if (!stats) return null;
const segments = chart.valueOrder
.map((value) => ({ name: value, value: stats.counts[value] ?? 0 }))
.filter((s) => s.value > 0);
const total = segments.reduce((sum, s) => sum + s.value, 0);
if (total === 0) return null;
return (
<div
key={ts(chart.label)}
className="bg-warm-50 dark:bg-warm-800 rounded p-2"
>
<div className="flex justify-between items-baseline mb-1.5">
{featureMeta ? (
<FeatureLabel
feature={featureMeta}
onShowInfo={setInfoFeature}
className="mr-2"
/>
) : (
<span className="text-xs text-warm-700 dark:text-warm-300 truncate mr-2">
{ts(chart.label)}
</span>
)}
<span className="text-xs font-semibold text-teal-700 dark:text-teal-400 whitespace-nowrap">
{total.toLocaleString()}
</span>
</div>
<StackedBarChart
segments={segments}
total={total}
colorMap={Object.fromEntries(
chart.valueOrder.map((v, i) => [v, chart.valueColors[i]])
)}
/>
) : (
<span className="text-xs text-warm-700 dark:text-warm-300">
{ts(chart.label)}
</span>
)}
</div>
);
}
const components = chart.components
.map((name) => {
const stats = enumByName.get(name);
return stats ? { label: name, stats } : null;
})
.filter((c): c is NonNullable<typeof c> => c !== null);
if (components.length === 0) return null;
return (
<div
key={ts(chart.label)}
className="bg-warm-50 dark:bg-warm-800 rounded p-2"
>
<div className="mb-1.5">
{featureMeta ? (
<FeatureLabel
feature={{ ...featureMeta, name: ts(chart.label) }}
onShowInfo={setInfoFeature}
/>
) : (
<span className="text-xs text-warm-700 dark:text-warm-300">
{ts(chart.label)}
</span>
)}
</div>
<StackedEnumChart
components={components}
valueOrder={chart.valueOrder}
valueColors={chart.valueColors}
/>
</div>
<StackedEnumChart
components={components}
valueOrder={chart.valueOrder}
valueColors={chart.valueColors}
/>
</div>
);
})}
</div>
)}
</div>
);
})}
</div>
) : null}
);
})}
</div>
)}
</div>
);
})}
</div>
) : null}
</div>
</div>