feat(ui): collapsible legend gives the heatmap more vertical room
The legend below the heatmap used four full prose paragraphs at
all times — visually it claimed about a quarter of the heatmap
page's height while the plot itself was the actual artefact.
Now the legend defaults to a single-line header strip:
Legende ↔ Schaden ↕ Nutzen ● Betroffenheit
◐ Evidenz-Stufe ⌄
Tap anywhere on the strip and an AnimatedSize transition opens
the full _LegendRow descriptions (180 ms). Same content, hidden
until asked for. The Expanded(heatmap) above reclaims the
height — concretely on a 900 px window the plot grows from
~420 px to ~600 px tall, which is what makes hover-targeting
the smaller points actually comfortable.
Signed-off-by: flemming-it <sf@flemming.it>
This commit is contained in:
parent
7c92268238
commit
94f4473686
1 changed files with 157 additions and 61 deletions
|
|
@ -129,74 +129,170 @@ class _HeatmapPageState extends State<HeatmapPage> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Axis + circle legend below the heatmap. Spells out what is
|
/// Axis + circle legend below the heatmap.
|
||||||
/// plotted and how — the four-line top description deliberately
|
///
|
||||||
/// stays short and high-level, this card carries the precise
|
/// Collapsed default: one-line header row with mini-labels for
|
||||||
/// reading instructions a reviewer needs to defend the plot
|
/// Schaden / Nutzen / Betroffenheit / Evidenz-Stufe + chevron —
|
||||||
/// against "what does that even mean?"
|
/// keeps the heatmap visually dominant and gives back vertical
|
||||||
class _LegendCard extends StatelessWidget {
|
/// 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();
|
const _LegendCard();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_LegendCard> createState() => _LegendCardState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LegendCardState extends State<_LegendCard> {
|
||||||
|
bool _expanded = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final t = Theme.of(context).textTheme;
|
final t = Theme.of(context).textTheme;
|
||||||
return ReclaimCard(
|
return ReclaimCard(
|
||||||
child: Column(
|
padding: const EdgeInsets.symmetric(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
horizontal: ReclaimSpace.lg,
|
||||||
children: [
|
vertical: ReclaimSpace.sm,
|
||||||
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.',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
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),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue