fix tap on chart 2

This commit is contained in:
davmar
2026-09-11 13:20:48 +02:00
parent cfb551328b
commit 34ef156538
+28 -185
View File
@@ -1431,13 +1431,18 @@ class _ChartCard extends StatelessWidget {
final hasLegend = metric == OverviewMetricMode.flow || final hasLegend = metric == OverviewMetricMode.flow ||
labelSpend.isNotEmpty; labelSpend.isNotEmpty;
final radius = BorderRadius.circular(compact ? 16 : 20);
return Card( return Card(
color: colors.surfaceContainer, color: colors.surfaceContainer,
elevation: 0, elevation: 0,
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
shape: RoundedRectangleBorder( clipBehavior: Clip.antiAlias,
borderRadius: BorderRadius.circular(compact ? 16 : 20), shape: RoundedRectangleBorder(borderRadius: radius),
), // Whole card opens the chart filter; nested buttons/legend keep their own taps.
child: InkWell(
onTap: onChartTap,
borderRadius: radius,
child: Padding( child: Padding(
padding: EdgeInsets.fromLTRB( padding: EdgeInsets.fromLTRB(
compact ? 12 : 14, compact ? 12 : 14,
@@ -1595,8 +1600,6 @@ class _ChartCard extends StatelessWidget {
], ],
SizedBox( SizedBox(
height: chartHeight, height: chartHeight,
child: _TappableChartArea(
onChartTap: onChartTap,
child: metric == OverviewMetricMode.flow child: metric == OverviewMetricMode.flow
? _FlowBars( ? _FlowBars(
months: months, months: months,
@@ -1618,77 +1621,14 @@ class _ChartCard extends StatelessWidget {
onChartTap: onChartTap, onChartTap: onChartTap,
), ),
), ),
),
], ],
), ),
), ),
);
}
}
/// Lets empty-chart taps open the chart filter without double-firing when a bar
/// was just selected (fl_chart + parent [GestureDetector] both see the tap).
class _TappableChartArea extends StatefulWidget {
const _TappableChartArea({
required this.onChartTap,
required this.child,
});
final VoidCallback onChartTap;
final Widget child;
@override
State<_TappableChartArea> createState() => _TappableChartAreaState();
}
class _TappableChartAreaState extends State<_TappableChartArea> {
var _handling = false;
void _runExclusive(VoidCallback action) {
if (_handling) return;
_handling = true;
action();
Future<void>.delayed(const Duration(milliseconds: 400), () {
if (mounted) _handling = false;
});
}
void openChartFilter() => _runExclusive(widget.onChartTap);
void openBar(VoidCallback action) => _runExclusive(action);
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: openChartFilter,
child: _ChartTapScope(
openChartFilter: openChartFilter,
openBar: openBar,
child: widget.child,
), ),
); );
} }
} }
class _ChartTapScope extends InheritedWidget {
const _ChartTapScope({
required this.openChartFilter,
required this.openBar,
required super.child,
});
final VoidCallback openChartFilter;
final void Function(VoidCallback action) openBar;
static _ChartTapScope? maybeOf(BuildContext context) {
return context.getInheritedWidgetOfExactType<_ChartTapScope>();
}
@override
bool updateShouldNotify(_ChartTapScope oldWidget) => false;
}
class _FlowBars extends StatelessWidget { class _FlowBars extends StatelessWidget {
const _FlowBars({ const _FlowBars({
required this.months, required this.months,
@@ -1742,51 +1682,20 @@ class _FlowBars extends StatelessWidget {
); );
} }
return BarChart( return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onChartTap,
child: IgnorePointer(
// fl_chart otherwise owns the gesture arena and empty taps go nowhere.
// Tap opens this chart's filters; legend / history still open columns.
child: BarChart(
BarChartData( BarChartData(
maxY: safeMax, maxY: safeMax,
groupsSpace: period == OverviewPeriodMode.yearly ? 16 : 10, groupsSpace: period == OverviewPeriodMode.yearly ? 16 : 10,
barTouchData: BarTouchData( barTouchData: const BarTouchData(enabled: false),
touchCallback: (event, response) {
if (event is! FlTapUpEvent) return;
final spot = response?.spot;
final index = spot?.touchedBarGroupIndex;
if (spot == null ||
index == null ||
index < 0 ||
index >= months.length) {
// Parent GestureDetector also handles empty-area taps.
return;
}
final rodIndex = spot.touchedRodDataIndex;
// Rods: 0 = expense, 1 = income.
final flow = rodIndex == 1
? TxFlowFilter.income
: TxFlowFilter.expense;
final scope = _ChartTapScope.maybeOf(context);
if (scope != null) {
scope.openBar(() => onMonthTap(months[index], flow: flow));
} else {
onMonthTap(months[index], flow: flow);
}
},
touchTooltipData: BarTouchTooltipData(
getTooltipItem: (group, groupIndex, rod, rodIndex) {
final month = months[group.x.toInt()];
final bucket = monthly[_periodKey(month, period)]!;
final isIncome = rodIndex == 1;
final value = isIncome ? bucket.income : bucket.expense;
return BarTooltipItem(
'${_periodAxisLabel(month, period)}\n'
'${isIncome ? 'Income' : 'Expense'}\n'
'${formatMoney(value, currency)}',
TextStyle(color: colors.onInverseSurface),
);
},
),
),
titlesData: FlTitlesData( titlesData: FlTitlesData(
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), topTitles:
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
rightTitles: rightTitles:
const AxisTitles(sideTitles: SideTitles(showTitles: false)), const AxisTitles(sideTitles: SideTitles(showTitles: false)),
leftTitles: AxisTitles( leftTitles: AxisTitles(
@@ -1869,6 +1778,8 @@ class _FlowBars extends StatelessWidget {
], ],
), ),
duration: Duration.zero, duration: Duration.zero,
),
),
); );
} }
} }
@@ -2078,86 +1989,16 @@ class _LabelMonthBars extends StatelessWidget {
final axisInterval = ((safeMax - safeMin) / 3).abs(); final axisInterval = ((safeMax - safeMin) / 3).abs();
final interval = axisInterval <= 0 ? 1.0 : axisInterval; final interval = axisInterval <= 0 ? 1.0 : axisInterval;
return BarChart( return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onChartTap,
child: IgnorePointer(
child: BarChart(
BarChartData( BarChartData(
minY: safeMin, minY: safeMin,
maxY: safeMax, maxY: safeMax,
groupsSpace: period == OverviewPeriodMode.yearly ? 16 : 10, groupsSpace: period == OverviewPeriodMode.yearly ? 16 : 10,
barTouchData: BarTouchData( barTouchData: const BarTouchData(enabled: false),
touchCallback: (event, response) {
if (event is! FlTapUpEvent) return;
final spot = response?.spot;
final index = spot?.touchedBarGroupIndex;
if (spot == null ||
index == null ||
index < 0 ||
index >= months.length) {
// Parent GestureDetector also handles empty-area taps.
return;
}
String? labelId;
if (stacked) {
final stackIdx = spot.touchedStackItemIndex;
if (stackIdx >= 0 && stackIdx < topLabels.length) {
labelId = topLabels[stackIdx].label.id;
}
} else {
final rodIdx = spot.touchedRodDataIndex;
if (rodIdx >= 0 && rodIdx < topLabels.length) {
labelId = topLabels[rodIdx].label.id;
}
// Last rod may be "Other" — open month with chart labels only.
}
final scope = _ChartTapScope.maybeOf(context);
if (scope != null) {
scope.openBar(
() => onMonthTap(months[index], labelId: labelId),
);
} else {
onMonthTap(months[index], labelId: labelId);
}
},
touchTooltipData: BarTouchTooltipData(
getTooltipItem: (group, groupIndex, rod, rodIndex) {
final month = months[group.x.toInt()];
if (stacked && rod.rodStackItems.isNotEmpty) {
final bucket =
amountByMonth[_periodKey(month, period)] ?? const {};
final buffer = StringBuffer(
'${_periodAxisLabel(month, period)}\n',
);
var total = 0.0;
for (final row in topLabels) {
final amount = bucket[row.label.id] ?? 0;
if (amount == 0) continue;
total += amount;
buffer.writeln(
'${row.label.name}: ${formatMoney(amount, currency)}',
);
}
final other = otherFor(month);
if (other != 0) {
total += other;
buffer.writeln('Other: ${formatMoney(other, currency)}');
}
buffer.write('Total: ${formatMoney(total, currency)}');
return BarTooltipItem(
buffer.toString(),
TextStyle(color: colors.onInverseSurface),
);
}
final name = rodIndex < topLabels.length
? topLabels[rodIndex].label.name
: 'Other';
return BarTooltipItem(
'${_periodAxisLabel(month, period)}\n'
'$name\n'
'${formatMoney(invertChart ? -rod.toY : rod.toY, currency)}',
TextStyle(color: colors.onInverseSurface),
);
},
),
),
titlesData: FlTitlesData( titlesData: FlTitlesData(
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
rightTitles: rightTitles:
@@ -2230,6 +2071,8 @@ class _LabelMonthBars extends StatelessWidget {
], ],
), ),
duration: Duration.zero, duration: Duration.zero,
),
),
); );
} }
} }