diff --git a/app/lib/pages/heatmap_page.dart b/app/lib/pages/heatmap_page.dart index fbf3a2e..20fad48 100644 --- a/app/lib/pages/heatmap_page.dart +++ b/app/lib/pages/heatmap_page.dart @@ -129,74 +129,170 @@ class _HeatmapPageState extends State { } } -/// 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 { +/// Axis + circle legend below the heatmap. +/// +/// Collapsed default: one-line header row with mini-labels for +/// Schaden / Nutzen / Betroffenheit / Evidenz-Stufe + chevron — +/// keeps the heatmap visually dominant and gives back vertical +/// space to the plot. Tap anywhere on the header expands the +/// full reading-instructions (axes, scale, sources, Tier rules). +class _LegendCard extends StatefulWidget { const _LegendCard(); + @override + State<_LegendCard> createState() => _LegendCardState(); +} + +class _LegendCardState extends State<_LegendCard> { + bool _expanded = false; + @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), - const _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), - const _LegendRow( - symbol: '↕', - axis: 'Ordinate (Y)', - label: 'Nutzen', - description: - 'Komposit-Score von 0–5, 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), - const _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), - const _LegendRow( - symbol: '◐', - axis: 'Farb-Kante', - label: 'Evidenz-Stufe', - description: - 'Jeder Wert trägt eine von vier Stufen — ' - 'T1 amtlich (NKR, DESTATIS, ifo, ZEW), ' - 'T2 Verband (DIHK, IHK, ZDH, BDI), ' - 'T3 eigene Erhebung, ' - 'T4 qualitatives Signal. ' - 'Eine Zahl bekommt die Stufe ihrer schwächsten ' - 'Eingangs-Variable — wenn h nur geschätzt ist (T4), ' - 'ist die ganze SKM-Zahl T4. Petrol → warm = stark → ' - 'schwach.', - ), - ], + padding: const EdgeInsets.symmetric( + horizontal: ReclaimSpace.lg, + vertical: ReclaimSpace.sm, ), + child: InkWell( + onTap: () => setState(() => _expanded = !_expanded), + borderRadius: + const BorderRadius.all(ReclaimRadius.sm), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: ReclaimSpace.xs, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text('Legende', + style: t.titleMedium?.copyWith( + color: ReclaimColors.mute, + )), + const SizedBox(width: ReclaimSpace.lg), + const Expanded( + child: Wrap( + spacing: ReclaimSpace.lg, + runSpacing: ReclaimSpace.xs, + children: [ + _MiniLabel(symbol: '↔', text: 'Schaden'), + _MiniLabel(symbol: '↕', text: 'Nutzen'), + _MiniLabel( + symbol: '●', text: 'Betroffenheit'), + _MiniLabel( + symbol: '◐', text: 'Evidenz-Stufe'), + ], + ), + ), + Icon( + _expanded + ? Icons.expand_less + : Icons.expand_more, + color: ReclaimColors.mute, + ), + ], + ), + AnimatedSize( + duration: const Duration(milliseconds: 180), + curve: Curves.easeOut, + child: _expanded + ? const Padding( + padding: + EdgeInsets.only(top: ReclaimSpace.lg), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + _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).', + ), + SizedBox(height: ReclaimSpace.sm), + _LegendRow( + symbol: '↕', + axis: 'Ordinate (Y)', + label: 'Nutzen', + description: + 'Komposit-Score von 0–5, Mittelwert ' + 'aus den sieben Dimensionen ' + 'Schutz · Markt · Rechtssicherheit · ' + 'EU-Binnenmarkt · Sicherheit · ' + 'Umwelt · Einnahmen (Studie §3.2). ' + 'Jede Dimension einzeln ' + 'quellen-begründet.', + ), + 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.', + ), + SizedBox(height: ReclaimSpace.sm), + _LegendRow( + symbol: '◐', + axis: 'Farb-Kante', + label: 'Evidenz-Stufe', + description: + 'Jeder Wert trägt eine von vier Stufen — ' + 'T1 amtlich (NKR, DESTATIS, ifo, ZEW), ' + 'T2 Verband (DIHK, IHK, ZDH, BDI), ' + 'T3 eigene Erhebung, ' + 'T4 qualitatives Signal. ' + 'Eine Zahl bekommt die Stufe ihrer ' + 'schwächsten Eingangs-Variable — wenn h ' + 'nur geschätzt ist (T4), ist die ganze ' + 'SKM-Zahl T4. Petrol → warm = stark → ' + 'schwach.', + ), + ], + ), + ) + : const SizedBox.shrink(), + ), + ], + ), + ), + ), + ); + } +} + +class _MiniLabel extends StatelessWidget { + const _MiniLabel({required this.symbol, required this.text}); + + final String symbol; + final String text; + + @override + Widget build(BuildContext context) { + final t = Theme.of(context).textTheme; + return Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text(symbol, + style: t.titleMedium?.copyWith( + color: ReclaimColors.signal, + )), + const SizedBox(width: 6), + Text(text, style: t.labelLarge), + ], ); } }