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,58 +129,121 @@ 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 {
|
||||
/// 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(
|
||||
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: [
|
||||
Text('Legende', style: t.headlineSmall),
|
||||
const SizedBox(height: ReclaimSpace.md),
|
||||
const _LegendRow(
|
||||
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). '
|
||||
'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(
|
||||
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 '
|
||||
'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(
|
||||
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. '
|
||||
'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(
|
||||
SizedBox(height: ReclaimSpace.sm),
|
||||
_LegendRow(
|
||||
symbol: '◐',
|
||||
axis: 'Farb-Kante',
|
||||
label: 'Evidenz-Stufe',
|
||||
|
|
@ -190,13 +253,46 @@ class _LegendCard extends StatelessWidget {
|
|||
'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 → '
|
||||
'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