feat(ui): add legend card below the heatmap

The four-line description above the heatmap was concise but
opaque: 'X: log10 €/year. Y: 0–5. Punktgröße.' said the right
words, but a reviewer who isn't already methodologically
oriented can't tell what they're looking at.

Adds a dedicated Legende-Karte directly under the heatmap.
Four rows, each: leading symbol (↔ ↕ ● ◐), axis-or-quantity
label, prominent name (Schaden / Nutzen / Betroffenheit /
Evidenz-Stufe), and a short prose explanation tying the visual
back to the Studie:

  ↔  Abszisse (X)  Schaden       — log10 €/Jahr, SKM §3.1
  ↕  Ordinate (Y)  Nutzen        — 7-dim. Komposit §3.2
  ●  Punktgröße    Betroffenheit — sqrt scale, DESTATIS-/IHK
  ◐  Tier-Farbe    Evidenz-Stufe — Regel der niedrigsten §6.7

Top description tightened to one line of context only:
'Schaden × Nutzen × Betroffenheit pro Norm. Klick auf einen
Punkt öffnet das Detail …'. No redundancy with the legend,
no jargon up top.

Signed-off-by: flemming-it <sf@flemming.it>
This commit is contained in:
flemming-it 2026-06-18 14:41:07 +02:00
parent 91a87e2bd8
commit f61865a3c2

View file

@ -50,9 +50,9 @@ class _HeatmapPageState extends State<HeatmapPage> {
Text('Heatmap', style: t.displaySmall), Text('Heatmap', style: t.displaySmall),
const SizedBox(height: ReclaimSpace.xs), const SizedBox(height: ReclaimSpace.xs),
Text( Text(
'X: jährliche Bürokratiekosten (€, log10). ' 'Schaden × Nutzen × Betroffenheit pro Norm. '
'Y: Nutzen-Score 05. Punktgröße: Betroffenheit. ' 'Klick auf einen Punkt öffnet das Detail mit '
'Klick auf einen Punkt → Detail mit Pflichten + Quellen.', 'Pflichten, Quellen und Gesetzestext.',
style: t.bodyLarge?.copyWith( style: t.bodyLarge?.copyWith(
color: ReclaimColors.mute, color: ReclaimColors.mute,
), ),
@ -77,6 +77,8 @@ class _HeatmapPageState extends State<HeatmapPage> {
), ),
), ),
), ),
const SizedBox(height: ReclaimSpace.md),
_LegendCard(),
const SizedBox(height: ReclaimSpace.lg), const SizedBox(height: ReclaimSpace.lg),
Wrap( Wrap(
spacing: ReclaimSpace.md, spacing: ReclaimSpace.md,
@ -126,3 +128,134 @@ class _HeatmapPageState extends State<HeatmapPage> {
); );
} }
} }
/// Axis + circle legend below the heatmap. Spells out what is
/// plotted and how the four-line top description deliberately
/// stays short and high-level, this card carries the precise
/// reading instructions a reviewer needs to defend the plot
/// against "what does that even mean?"
class _LegendCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final t = Theme.of(context).textTheme;
return ReclaimCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Legende', style: t.headlineSmall),
const SizedBox(height: ReclaimSpace.md),
_LegendRow(
symbol: '',
axis: 'Abszisse (X)',
label: 'Schaden',
description:
'Bürokratiekosten pro Jahr in Euro, logarithmisch '
'skaliert (10⁴ = 10 k, 10⁶ = 1 Mio, 10⁹ = 1 Mrd). '
'Berechnet nach Standardkostenmodell '
'P × F × T × h (Studie §3.1).',
),
const SizedBox(height: ReclaimSpace.sm),
_LegendRow(
symbol: '',
axis: 'Ordinate (Y)',
label: 'Nutzen',
description:
'Komposit-Score von 05, Mittelwert aus den sieben '
'Dimensionen Schutz · Markt · Rechtssicherheit · '
'EU-Binnenmarkt · Sicherheit · Umwelt · Einnahmen '
'(Studie §3.2). Jede Dimension einzeln '
'quellen-begründet.',
),
const SizedBox(height: ReclaimSpace.sm),
_LegendRow(
symbol: '',
axis: 'Punktgröße',
label: 'Betroffenheit',
description:
'Anzahl der Adressaten (KMU, Bürger, Behörden), '
'wurzel-skaliert. Mehr Adressaten → größerer Kreis. '
'Quellen: DESTATIS Unternehmensregister, '
'IHK-/Handwerkskammer-Mitgliederzahlen.',
),
const SizedBox(height: ReclaimSpace.sm),
_LegendRow(
symbol: '',
axis: 'Tier-Farbe',
label: 'Evidenz-Stufe',
description:
'Petrol → warm-orange markiert die Stufe (T1T4) der '
'schwächsten Eingangs-Variable. Eine SKM-Zahl mit '
'T4-Schätzwert für h trägt die ganze Zahl als T4 '
'(Regel der niedrigsten Stufe, Studie §6.7).',
),
],
),
);
}
}
class _LegendRow extends StatelessWidget {
const _LegendRow({
required this.symbol,
required this.axis,
required this.label,
required this.description,
});
final String symbol;
final String axis;
final String label;
final String description;
@override
Widget build(BuildContext context) {
final t = Theme.of(context).textTheme;
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 28,
child: Text(
symbol,
style: t.titleMedium?.copyWith(
color: ReclaimColors.signal,
fontSize: 18,
),
),
),
SizedBox(
width: 140,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
axis,
style: t.labelSmall?.copyWith(
color: ReclaimColors.mute,
letterSpacing: 0.4,
),
),
Text(
label,
style: t.titleMedium,
),
],
),
),
const SizedBox(width: ReclaimSpace.md),
Expanded(
child: Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(
description,
style: t.bodyLarge?.copyWith(
color: ReclaimColors.mute,
height: 1.5,
),
),
),
),
],
);
}
}