fix tap on chart 2
This commit is contained in:
@@ -1431,13 +1431,18 @@ class _ChartCard extends StatelessWidget {
|
||||
final hasLegend = metric == OverviewMetricMode.flow ||
|
||||
labelSpend.isNotEmpty;
|
||||
|
||||
final radius = BorderRadius.circular(compact ? 16 : 20);
|
||||
|
||||
return Card(
|
||||
color: colors.surfaceContainer,
|
||||
elevation: 0,
|
||||
margin: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(compact ? 16 : 20),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
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(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
compact ? 12 : 14,
|
||||
@@ -1595,8 +1600,6 @@ class _ChartCard extends StatelessWidget {
|
||||
],
|
||||
SizedBox(
|
||||
height: chartHeight,
|
||||
child: _TappableChartArea(
|
||||
onChartTap: onChartTap,
|
||||
child: metric == OverviewMetricMode.flow
|
||||
? _FlowBars(
|
||||
months: months,
|
||||
@@ -1618,77 +1621,14 @@ class _ChartCard extends StatelessWidget {
|
||||
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 {
|
||||
const _FlowBars({
|
||||
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(
|
||||
maxY: safeMax,
|
||||
groupsSpace: period == OverviewPeriodMode.yearly ? 16 : 10,
|
||||
barTouchData: BarTouchData(
|
||||
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),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
barTouchData: const BarTouchData(enabled: false),
|
||||
titlesData: FlTitlesData(
|
||||
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
topTitles:
|
||||
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
rightTitles:
|
||||
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
leftTitles: AxisTitles(
|
||||
@@ -1869,6 +1778,8 @@ class _FlowBars extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
duration: Duration.zero,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2078,86 +1989,16 @@ class _LabelMonthBars extends StatelessWidget {
|
||||
final axisInterval = ((safeMax - safeMin) / 3).abs();
|
||||
final interval = axisInterval <= 0 ? 1.0 : axisInterval;
|
||||
|
||||
return BarChart(
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onChartTap,
|
||||
child: IgnorePointer(
|
||||
child: BarChart(
|
||||
BarChartData(
|
||||
minY: safeMin,
|
||||
maxY: safeMax,
|
||||
groupsSpace: period == OverviewPeriodMode.yearly ? 16 : 10,
|
||||
barTouchData: BarTouchData(
|
||||
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),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
barTouchData: const BarTouchData(enabled: false),
|
||||
titlesData: FlTitlesData(
|
||||
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
rightTitles:
|
||||
@@ -2230,6 +2071,8 @@ class _LabelMonthBars extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
duration: Duration.zero,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user