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>
261 lines
8.7 KiB
Dart
261 lines
8.7 KiB
Dart
import 'package:flutter/material.dart';
|
||
|
||
import '../data/models.dart';
|
||
import '../data/repository.dart';
|
||
import '../theme/reclaim_tokens.dart';
|
||
import '../widgets/demo_banner.dart';
|
||
import '../widgets/heatmap_grid.dart';
|
||
import '../widgets/reclaim_card.dart';
|
||
import '../widgets/tier_badge.dart';
|
||
import 'norm_detail_page.dart';
|
||
|
||
class HeatmapPage extends StatefulWidget {
|
||
const HeatmapPage({super.key, required this.repository});
|
||
|
||
final EvaluationRepository repository;
|
||
|
||
@override
|
||
State<HeatmapPage> createState() => _HeatmapPageState();
|
||
}
|
||
|
||
class _HeatmapPageState extends State<HeatmapPage> {
|
||
late Future<List<Evaluation>> _future;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_future = widget.repository.list();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final t = Theme.of(context).textTheme;
|
||
return Column(
|
||
children: [
|
||
const DemoBanner(),
|
||
Expanded(
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(ReclaimSpace.xl),
|
||
child: FutureBuilder<List<Evaluation>>(
|
||
future: _future,
|
||
builder: (context, snap) {
|
||
if (!snap.hasData) {
|
||
return const Center(
|
||
child: CircularProgressIndicator());
|
||
}
|
||
final items = snap.data!;
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text('Heatmap', style: t.displaySmall),
|
||
const SizedBox(height: ReclaimSpace.xs),
|
||
Text(
|
||
'Schaden × Nutzen × Betroffenheit pro Norm. '
|
||
'Klick auf einen Punkt öffnet das Detail mit '
|
||
'Pflichten, Quellen und Gesetzestext.',
|
||
style: t.bodyLarge?.copyWith(
|
||
color: ReclaimColors.mute,
|
||
),
|
||
),
|
||
const SizedBox(height: ReclaimSpace.lg),
|
||
Expanded(
|
||
child: ReclaimCard(
|
||
accent: true,
|
||
expand: true,
|
||
child: HeatmapGrid(
|
||
evaluations: items,
|
||
onTap: (e) {
|
||
Navigator.of(context).push(
|
||
MaterialPageRoute(
|
||
builder: (_) => NormDetailPage(
|
||
repository: widget.repository,
|
||
evaluation: e,
|
||
),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: ReclaimSpace.md),
|
||
_LegendCard(),
|
||
const SizedBox(height: ReclaimSpace.lg),
|
||
Wrap(
|
||
spacing: ReclaimSpace.md,
|
||
runSpacing: ReclaimSpace.sm,
|
||
children: [
|
||
for (final e in items)
|
||
InkWell(
|
||
onTap: () {
|
||
Navigator.of(context).push(
|
||
MaterialPageRoute(
|
||
builder: (_) => NormDetailPage(
|
||
repository: widget.repository,
|
||
evaluation: e,
|
||
),
|
||
),
|
||
);
|
||
},
|
||
child: ReclaimCard(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: ReclaimSpace.md,
|
||
vertical: ReclaimSpace.sm,
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
TierBadge(
|
||
tier: e.tierLowest, compact: true),
|
||
const SizedBox(
|
||
width: ReclaimSpace.sm),
|
||
Text(
|
||
'${e.norm.jurabk} ${e.norm.paragraph}',
|
||
style: t.labelLarge,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
);
|
||
},
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 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 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),
|
||
_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 (T1–T4) 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,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|