fix tap on chart

This commit is contained in:
davmar
2026-09-11 12:45:50 +02:00
parent 40366f0708
commit cfb551328b
+159 -9
View File
@@ -487,6 +487,16 @@ class _OverviewScreenState extends State<OverviewScreen> {
: null), : null),
periodMode: favourite.period, periodMode: favourite.period,
), ),
onChartTap: () => _openChartFilter(
context,
labelIds: {
for (final id in favourite.labelIds) id,
},
flow: favourite.metric == OverviewMetricMode.labels
? favourite.flow
: null,
periodMode: favourite.period,
),
); );
} }
@@ -609,6 +619,10 @@ class _OverviewScreenState extends State<OverviewScreen> {
flow: flow ?? flow: flow ??
(_metric == OverviewMetricMode.labels ? _labelFlow : null), (_metric == OverviewMetricMode.labels ? _labelFlow : null),
), ),
onChartTap: () => _openChartFilter(
context,
flow: _metric == OverviewMetricMode.labels ? _labelFlow : null,
),
), ),
const SizedBox(height: 14), const SizedBox(height: 14),
if (_metric == OverviewMetricMode.flow) ...[ if (_metric == OverviewMetricMode.flow) ...[
@@ -784,6 +798,34 @@ class _OverviewScreenState extends State<OverviewScreen> {
), ),
); );
} }
/// Open Transactions with this chart's filters (no specific bar/column).
void _openChartFilter(
BuildContext context, {
Set<String>? labelIds,
TxFlowFilter? flow,
OverviewPeriodMode? periodMode,
}) {
final mode = periodMode ?? _period;
final yearly = mode == OverviewPeriodMode.yearly;
final resolvedLabelIds = labelIds ??
{
for (final id in _includedLabelIds) id,
};
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => TransactionsScreen(
// Monthly charts show a year — open that whole year with the chart filter.
initialMonth: yearly ? null : DateTime(_cursor.year, 12),
initialYear: yearly ? null : _cursor.year,
initialLabelIds: resolvedLabelIds,
initialLabelMatch: LabelMatchMode.or,
initialFlow: flow,
initialSearchInMonthOnly: false,
),
),
);
}
} }
class _MonthBucket { class _MonthBucket {
@@ -1183,6 +1225,7 @@ class _ChartCard extends StatelessWidget {
required this.availableLabelCount, required this.availableLabelCount,
required this.onLabelTap, required this.onLabelTap,
required this.onMonthTap, required this.onMonthTap,
required this.onChartTap,
this.onPeriodModeChanged, this.onPeriodModeChanged,
this.onEdit, this.onEdit,
}); });
@@ -1211,6 +1254,7 @@ class _ChartCard extends StatelessWidget {
String? labelId, String? labelId,
TxFlowFilter? flow, TxFlowFilter? flow,
}) onMonthTap; }) onMonthTap;
final VoidCallback onChartTap;
final ValueChanged<OverviewPeriodMode>? onPeriodModeChanged; final ValueChanged<OverviewPeriodMode>? onPeriodModeChanged;
final VoidCallback? onEdit; final VoidCallback? onEdit;
@@ -1551,6 +1595,8 @@ 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,
@@ -1558,6 +1604,7 @@ class _ChartCard extends StatelessWidget {
currency: currency, currency: currency,
period: period, period: period,
onMonthTap: onMonthTap, onMonthTap: onMonthTap,
onChartTap: onChartTap,
) )
: _LabelMonthBars( : _LabelMonthBars(
months: months, months: months,
@@ -1568,6 +1615,8 @@ class _ChartCard extends StatelessWidget {
barStyle: labelBarStyle, barStyle: labelBarStyle,
invertChart: invertChart, invertChart: invertChart,
onMonthTap: onMonthTap, onMonthTap: onMonthTap,
onChartTap: onChartTap,
),
), ),
), ),
], ],
@@ -1577,6 +1626,69 @@ class _ChartCard extends StatelessWidget {
} }
} }
/// 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,
@@ -1584,6 +1696,7 @@ class _FlowBars extends StatelessWidget {
required this.currency, required this.currency,
required this.period, required this.period,
required this.onMonthTap, required this.onMonthTap,
required this.onChartTap,
}); });
final List<DateTime> months; final List<DateTime> months;
@@ -1595,6 +1708,7 @@ class _FlowBars extends StatelessWidget {
String? labelId, String? labelId,
TxFlowFilter? flow, TxFlowFilter? flow,
}) onMonthTap; }) onMonthTap;
final VoidCallback onChartTap;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -1613,7 +1727,10 @@ class _FlowBars extends StatelessWidget {
}); });
if (empty) { if (empty) {
return Center( return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onChartTap,
child: Center(
child: Text( child: Text(
period == OverviewPeriodMode.yearly period == OverviewPeriodMode.yearly
? 'No income or expenses in these years' ? 'No income or expenses in these years'
@@ -1621,6 +1738,7 @@ class _FlowBars extends StatelessWidget {
style: theme.textTheme.bodyMedium, style: theme.textTheme.bodyMedium,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
),
); );
} }
@@ -1632,15 +1750,25 @@ class _FlowBars extends StatelessWidget {
touchCallback: (event, response) { touchCallback: (event, response) {
if (event is! FlTapUpEvent) return; if (event is! FlTapUpEvent) return;
final spot = response?.spot; final spot = response?.spot;
if (spot == null) return; final index = spot?.touchedBarGroupIndex;
final index = spot.touchedBarGroupIndex; if (spot == null ||
if (index < 0 || index >= months.length) return; index == null ||
index < 0 ||
index >= months.length) {
// Parent GestureDetector also handles empty-area taps.
return;
}
final rodIndex = spot.touchedRodDataIndex; final rodIndex = spot.touchedRodDataIndex;
// Rods: 0 = expense, 1 = income. // Rods: 0 = expense, 1 = income.
final flow = rodIndex == 1 final flow = rodIndex == 1
? TxFlowFilter.income ? TxFlowFilter.income
: TxFlowFilter.expense; : TxFlowFilter.expense;
final scope = _ChartTapScope.maybeOf(context);
if (scope != null) {
scope.openBar(() => onMonthTap(months[index], flow: flow));
} else {
onMonthTap(months[index], flow: flow); onMonthTap(months[index], flow: flow);
}
}, },
touchTooltipData: BarTouchTooltipData( touchTooltipData: BarTouchTooltipData(
getTooltipItem: (group, groupIndex, rod, rodIndex) { getTooltipItem: (group, groupIndex, rod, rodIndex) {
@@ -1755,6 +1883,7 @@ class _LabelMonthBars extends StatelessWidget {
required this.barStyle, required this.barStyle,
required this.invertChart, required this.invertChart,
required this.onMonthTap, required this.onMonthTap,
required this.onChartTap,
}); });
static const _maxSeriesLabels = 6; static const _maxSeriesLabels = 6;
@@ -1771,6 +1900,7 @@ class _LabelMonthBars extends StatelessWidget {
String? labelId, String? labelId,
TxFlowFilter? flow, TxFlowFilter? flow,
}) onMonthTap; }) onMonthTap;
final VoidCallback onChartTap;
double _plot(double value) => invertChart ? -value : value; double _plot(double value) => invertChart ? -value : value;
@@ -1779,12 +1909,16 @@ class _LabelMonthBars extends StatelessWidget {
final theme = Theme.of(context); final theme = Theme.of(context);
final colors = theme.colorScheme; final colors = theme.colorScheme;
if (labelSpend.isEmpty) { if (labelSpend.isEmpty) {
return Center( return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onChartTap,
child: Center(
child: Text( child: Text(
'No labeled activity in this period.\nTag transactions to see them here.', 'No labeled activity in this period.\nTag transactions to see them here.',
style: theme.textTheme.bodyMedium, style: theme.textTheme.bodyMedium,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
),
); );
} }
@@ -1841,12 +1975,16 @@ class _LabelMonthBars extends StatelessWidget {
} }
} }
if (maxY == 0 && minY == 0) { if (maxY == 0 && minY == 0) {
return Center( return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onChartTap,
child: Center(
child: Text( child: Text(
'No labeled activity in this period.\nTag transactions to see them here.', 'No labeled activity in this period.\nTag transactions to see them here.',
style: theme.textTheme.bodyMedium, style: theme.textTheme.bodyMedium,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
),
); );
} }
final span = (maxY - minY).abs(); final span = (maxY - minY).abs();
@@ -1949,9 +2087,14 @@ class _LabelMonthBars extends StatelessWidget {
touchCallback: (event, response) { touchCallback: (event, response) {
if (event is! FlTapUpEvent) return; if (event is! FlTapUpEvent) return;
final spot = response?.spot; final spot = response?.spot;
if (spot == null) return; final index = spot?.touchedBarGroupIndex;
final index = spot.touchedBarGroupIndex; if (spot == null ||
if (index < 0 || index >= months.length) return; index == null ||
index < 0 ||
index >= months.length) {
// Parent GestureDetector also handles empty-area taps.
return;
}
String? labelId; String? labelId;
if (stacked) { if (stacked) {
final stackIdx = spot.touchedStackItemIndex; final stackIdx = spot.touchedStackItemIndex;
@@ -1965,7 +2108,14 @@ class _LabelMonthBars extends StatelessWidget {
} }
// Last rod may be "Other" — open month with chart labels only. // 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); onMonthTap(months[index], labelId: labelId);
}
}, },
touchTooltipData: BarTouchTooltipData( touchTooltipData: BarTouchTooltipData(
getTooltipItem: (group, groupIndex, rod, rodIndex) { getTooltipItem: (group, groupIndex, rod, rodIndex) {