fix(mail): legend rows justify-between + per-connection chart sparse-data zoom

1. Donut-Legend-Rows als space-between: Name links + dot, Count rechts.
   Vorher: alle Elemente eng aneinander (gap:6), Count direkt nach Name.
   Jetzt: feste Legend-Width 180px, jede Row hat Name+Dot links (flex:1)
   und Count rechts mit Whitespace dazwischen.

2. Per-Connection-Bar-Chart in Account-Card: sparse-data-zoom.
   Vorher: bei nonEmpty.length > 0 && days <= 7 wurde gezoomt — bei 30-Tage-
   Range mit nur 1-2 Hits passierte das aber NICHT → 30 leere Bars + 1 Bar
   ganz rechts (Screenshot bei GMX-expanded).
   Jetzt: zoom IMMER wenn nonEmpty.length * 3 < raw.length (= mehr als
   2/3 der Range sind leer). Trim auf die echte Hit-Range. User sieht
   damit nur die Tage mit Daten + die paar dazwischen, statt 30 leere
   Slots.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-05-14 00:48:51 +02:00
parent aac6c00720
commit b47ac2427e
2 changed files with 34 additions and 23 deletions

View File

@ -112,7 +112,7 @@ export function MailDistributionChart({ data, hero, totalBlocked, isLegend }: Pr
centerLabel={centerLabel} centerLabel={centerLabel}
width={DONUT_WIDTH} width={DONUT_WIDTH}
/> />
<View style={{ gap: 6 }}> <View style={{ gap: 6, width: 180 }}>
{slices.map((slice) => ( {slices.map((slice) => (
<LegendRow key={slice.label} slice={slice} colors={colors} /> <LegendRow key={slice.label} slice={slice} colors={colors} />
))} ))}
@ -159,7 +159,15 @@ function LegendRow({
colors: ReturnType<typeof useColors>; colors: ReturnType<typeof useColors>;
}) { }) {
return ( return (
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}> <View
style={{
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: 8,
}}
>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6, flex: 1, minWidth: 0 }}>
<View <View
style={{ style={{
width: 8, width: 8,
@ -173,12 +181,13 @@ function LegendRow({
fontSize: 12, fontSize: 12,
fontFamily: slice.isOther ? 'Nunito_400Regular' : 'Nunito_600SemiBold', fontFamily: slice.isOther ? 'Nunito_400Regular' : 'Nunito_600SemiBold',
color: slice.isOther ? colors.textMuted : colors.text, color: slice.isOther ? colors.textMuted : colors.text,
maxWidth: 160, flexShrink: 1,
}} }}
numberOfLines={1} numberOfLines={1}
> >
{slice.label} {slice.label}
</Text> </Text>
</View>
<Text <Text
style={{ style={{
fontSize: 12, fontSize: 12,

View File

@ -73,8 +73,10 @@ export function useMailConnectionStats(
data = aggregateToWeeks(raw); data = aggregateToWeeks(raw);
} else if (granularity === 'month') { } else if (granularity === 'month') {
data = aggregateToMonths(raw); data = aggregateToMonths(raw);
} else if (nonEmpty.length > 0 && days <= 7) { } else if (nonEmpty.length > 0 && nonEmpty.length * 3 < raw.length) {
// Short window: keep only days with data + days between first and last hit // Sparse data (z.B. nur 1-2 Tage von 30): zoom in auf die echte Range
// zwischen erstem und letztem Hit. Vermeidet 30 leere Bars + 1 Bar
// ganz rechts wie bei einer frischen Outlook-Connection.
const firstDate = nonEmpty[0].date; const firstDate = nonEmpty[0].date;
const lastDate = nonEmpty[nonEmpty.length - 1].date; const lastDate = nonEmpty[nonEmpty.length - 1].date;
data = raw.filter((e) => e.date >= firstDate && e.date <= lastDate); data = raw.filter((e) => e.date >= firstDate && e.date <= lastDate);