fix tap on chart 2
This commit is contained in:
+267
-424
@@ -1431,172 +1431,175 @@ 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),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
compact ? 12 : 14,
|
||||
compact ? 10 : 12,
|
||||
compact ? 10 : 14,
|
||||
compact ? 12 : 16,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
if (compact) ...[
|
||||
Row(
|
||||
children: [
|
||||
if (onPeriodModeChanged != null)
|
||||
SegmentedButton<OverviewPeriodMode>(
|
||||
showSelectedIcon: false,
|
||||
style: _compactSegmentStyle,
|
||||
segments: const [
|
||||
ButtonSegment(
|
||||
value: OverviewPeriodMode.monthly,
|
||||
label: Text('Month'),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: OverviewPeriodMode.yearly,
|
||||
label: Text('Year'),
|
||||
),
|
||||
],
|
||||
selected: {period},
|
||||
onSelectionChanged: (selection) {
|
||||
onPeriodModeChanged!(selection.first);
|
||||
},
|
||||
),
|
||||
if (heading != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
heading!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
] else
|
||||
const Spacer(),
|
||||
if (showFilter) ...[
|
||||
const SizedBox(width: 4),
|
||||
_filterButton(colors),
|
||||
],
|
||||
if (onEdit != null) ...[
|
||||
const SizedBox(width: 2),
|
||||
_editButton(colors, compact: true),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: _periodControls(theme, compact: true),
|
||||
),
|
||||
if (hasLegend) ...[
|
||||
const SizedBox(width: 8),
|
||||
Flexible(child: legend),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
] else ...[
|
||||
if (heading != null) ...[
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
heading!,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
if (period == OverviewPeriodMode.monthly)
|
||||
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,
|
||||
compact ? 10 : 12,
|
||||
compact ? 10 : 14,
|
||||
compact ? 12 : 16,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
if (compact) ...[
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
tooltip: 'Previous year',
|
||||
onPressed: canGoBack ? onBack : null,
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -4,
|
||||
if (onPeriodModeChanged != null)
|
||||
SegmentedButton<OverviewPeriodMode>(
|
||||
showSelectedIcon: false,
|
||||
style: _compactSegmentStyle,
|
||||
segments: const [
|
||||
ButtonSegment(
|
||||
value: OverviewPeriodMode.monthly,
|
||||
label: Text('Month'),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: OverviewPeriodMode.yearly,
|
||||
label: Text('Year'),
|
||||
),
|
||||
],
|
||||
selected: {period},
|
||||
onSelectionChanged: (selection) {
|
||||
onPeriodModeChanged!(selection.first);
|
||||
},
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 32,
|
||||
minHeight: 32,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
icon: const Icon(Icons.chevron_left, size: 22),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
periodTitle,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
if (heading != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
heading!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Next year',
|
||||
onPressed: canGoForward ? onForward : null,
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -4,
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 32,
|
||||
minHeight: 32,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
icon: const Icon(Icons.chevron_right, size: 22),
|
||||
),
|
||||
] else
|
||||
const Spacer(),
|
||||
if (showFilter) ...[
|
||||
const SizedBox(width: 2),
|
||||
const SizedBox(width: 4),
|
||||
_filterButton(colors),
|
||||
],
|
||||
if (onEdit != null) ...[
|
||||
const SizedBox(width: 2),
|
||||
_editButton(colors, compact: true),
|
||||
],
|
||||
],
|
||||
)
|
||||
else
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(width: showFilter ? 36 : 0),
|
||||
Expanded(
|
||||
child: Text(
|
||||
periodTitle,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
child: _periodControls(theme, compact: true),
|
||||
),
|
||||
if (hasLegend) ...[
|
||||
const SizedBox(width: 8),
|
||||
Flexible(child: legend),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
] else ...[
|
||||
if (heading != null) ...[
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
heading!,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
if (showFilter)
|
||||
_filterButton(colors)
|
||||
else
|
||||
const SizedBox(width: 0),
|
||||
],
|
||||
),
|
||||
if (hasLegend) ...[
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: legend,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
if (period == OverviewPeriodMode.monthly)
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
tooltip: 'Previous year',
|
||||
onPressed: canGoBack ? onBack : null,
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -4,
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 32,
|
||||
minHeight: 32,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
icon: const Icon(Icons.chevron_left, size: 22),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
periodTitle,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Next year',
|
||||
onPressed: canGoForward ? onForward : null,
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -4,
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 32,
|
||||
minHeight: 32,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
icon: const Icon(Icons.chevron_right, size: 22),
|
||||
),
|
||||
if (showFilter) ...[
|
||||
const SizedBox(width: 2),
|
||||
_filterButton(colors),
|
||||
],
|
||||
],
|
||||
)
|
||||
else
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(width: showFilter ? 36 : 0),
|
||||
Expanded(
|
||||
child: Text(
|
||||
periodTitle,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showFilter)
|
||||
_filterButton(colors)
|
||||
else
|
||||
const SizedBox(width: 0),
|
||||
],
|
||||
),
|
||||
if (hasLegend) ...[
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: legend,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
],
|
||||
],
|
||||
SizedBox(
|
||||
height: chartHeight,
|
||||
child: _TappableChartArea(
|
||||
onChartTap: onChartTap,
|
||||
SizedBox(
|
||||
height: chartHeight,
|
||||
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,133 +1682,104 @@ class _FlowBars extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
return 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),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
rightTitles:
|
||||
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 42,
|
||||
interval: safeMax / 3,
|
||||
getTitlesWidget: (value, meta) {
|
||||
if (value <= 0) {
|
||||
return Text('0', style: theme.textTheme.labelSmall);
|
||||
}
|
||||
if ((value - safeMax).abs() < 0.01) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Text(
|
||||
_compactMoney(value),
|
||||
style: theme.textTheme.labelSmall,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 28,
|
||||
getTitlesWidget: (value, meta) {
|
||||
final index = value.toInt();
|
||||
if (index < 0 || index >= months.length) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
if (period == OverviewPeriodMode.monthly &&
|
||||
index % 2 == 1 &&
|
||||
months.length > 8) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(
|
||||
_periodAxisLabel(months[index], period),
|
||||
style: theme.textTheme.labelSmall,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
horizontalInterval: safeMax / 3,
|
||||
getDrawingHorizontalLine: (value) => FlLine(
|
||||
color: colors.outlineVariant.withValues(alpha: 0.35),
|
||||
strokeWidth: 1,
|
||||
dashArray: const [4, 4],
|
||||
),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
barGroups: [
|
||||
for (var i = 0; i < months.length; i++)
|
||||
BarChartGroupData(
|
||||
x: i,
|
||||
barsSpace: 3,
|
||||
barRods: [
|
||||
BarChartRodData(
|
||||
toY: monthly[_periodKey(months[i], period)]!.expense,
|
||||
width: period == OverviewPeriodMode.yearly ? 14 : 9,
|
||||
borderRadius:
|
||||
const BorderRadius.vertical(top: Radius.circular(6)),
|
||||
color: _kExpenseColor,
|
||||
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: const BarTouchData(enabled: false),
|
||||
titlesData: FlTitlesData(
|
||||
topTitles:
|
||||
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
rightTitles:
|
||||
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 42,
|
||||
interval: safeMax / 3,
|
||||
getTitlesWidget: (value, meta) {
|
||||
if (value <= 0) {
|
||||
return Text('0', style: theme.textTheme.labelSmall);
|
||||
}
|
||||
if ((value - safeMax).abs() < 0.01) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Text(
|
||||
_compactMoney(value),
|
||||
style: theme.textTheme.labelSmall,
|
||||
);
|
||||
},
|
||||
),
|
||||
BarChartRodData(
|
||||
toY: monthly[_periodKey(months[i], period)]!.income,
|
||||
width: period == OverviewPeriodMode.yearly ? 14 : 9,
|
||||
borderRadius:
|
||||
const BorderRadius.vertical(top: Radius.circular(6)),
|
||||
color: _kIncomeColor,
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 28,
|
||||
getTitlesWidget: (value, meta) {
|
||||
final index = value.toInt();
|
||||
if (index < 0 || index >= months.length) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
if (period == OverviewPeriodMode.monthly &&
|
||||
index % 2 == 1 &&
|
||||
months.length > 8) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(
|
||||
_periodAxisLabel(months[index], period),
|
||||
style: theme.textTheme.labelSmall,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
horizontalInterval: safeMax / 3,
|
||||
getDrawingHorizontalLine: (value) => FlLine(
|
||||
color: colors.outlineVariant.withValues(alpha: 0.35),
|
||||
strokeWidth: 1,
|
||||
dashArray: const [4, 4],
|
||||
),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
barGroups: [
|
||||
for (var i = 0; i < months.length; i++)
|
||||
BarChartGroupData(
|
||||
x: i,
|
||||
barsSpace: 3,
|
||||
barRods: [
|
||||
BarChartRodData(
|
||||
toY: monthly[_periodKey(months[i], period)]!.expense,
|
||||
width: period == OverviewPeriodMode.yearly ? 14 : 9,
|
||||
borderRadius:
|
||||
const BorderRadius.vertical(top: Radius.circular(6)),
|
||||
color: _kExpenseColor,
|
||||
),
|
||||
BarChartRodData(
|
||||
toY: monthly[_periodKey(months[i], period)]!.income,
|
||||
width: period == OverviewPeriodMode.yearly ? 14 : 9,
|
||||
borderRadius:
|
||||
const BorderRadius.vertical(top: Radius.circular(6)),
|
||||
color: _kIncomeColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
duration: Duration.zero,
|
||||
),
|
||||
),
|
||||
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(
|
||||
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),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onChartTap,
|
||||
child: IgnorePointer(
|
||||
child: BarChart(
|
||||
BarChartData(
|
||||
minY: safeMin,
|
||||
maxY: safeMax,
|
||||
groupsSpace: period == OverviewPeriodMode.yearly ? 16 : 10,
|
||||
barTouchData: const BarTouchData(enabled: false),
|
||||
titlesData: FlTitlesData(
|
||||
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
rightTitles:
|
||||
@@ -2220,16 +2061,18 @@ class _LabelMonthBars extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
barGroups: [
|
||||
for (var i = 0; i < months.length; i++)
|
||||
BarChartGroupData(
|
||||
x: i,
|
||||
barsSpace: stacked ? 0 : 2,
|
||||
barRods: rodsFor(months[i]),
|
||||
),
|
||||
],
|
||||
barGroups: [
|
||||
for (var i = 0; i < months.length; i++)
|
||||
BarChartGroupData(
|
||||
x: i,
|
||||
barsSpace: stacked ? 0 : 2,
|
||||
barRods: rodsFor(months[i]),
|
||||
),
|
||||
],
|
||||
),
|
||||
duration: Duration.zero,
|
||||
),
|
||||
),
|
||||
duration: Duration.zero,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user