fix tap on chart 2
This commit is contained in:
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user