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 ||
|
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: Padding(
|
child: InkWell(
|
||||||
padding: EdgeInsets.fromLTRB(
|
onTap: onChartTap,
|
||||||
compact ? 12 : 14,
|
borderRadius: radius,
|
||||||
compact ? 10 : 12,
|
child: Padding(
|
||||||
compact ? 10 : 14,
|
padding: EdgeInsets.fromLTRB(
|
||||||
compact ? 12 : 16,
|
compact ? 12 : 14,
|
||||||
),
|
compact ? 10 : 12,
|
||||||
child: Column(
|
compact ? 10 : 14,
|
||||||
children: [
|
compact ? 12 : 16,
|
||||||
if (compact) ...[
|
),
|
||||||
Row(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
if (onPeriodModeChanged != null)
|
if (compact) ...[
|
||||||
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)
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
if (onPeriodModeChanged != null)
|
||||||
tooltip: 'Previous year',
|
SegmentedButton<OverviewPeriodMode>(
|
||||||
onPressed: canGoBack ? onBack : null,
|
showSelectedIcon: false,
|
||||||
visualDensity: const VisualDensity(
|
style: _compactSegmentStyle,
|
||||||
horizontal: -4,
|
segments: const [
|
||||||
vertical: -4,
|
ButtonSegment(
|
||||||
|
value: OverviewPeriodMode.monthly,
|
||||||
|
label: Text('Month'),
|
||||||
|
),
|
||||||
|
ButtonSegment(
|
||||||
|
value: OverviewPeriodMode.yearly,
|
||||||
|
label: Text('Year'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
selected: {period},
|
||||||
|
onSelectionChanged: (selection) {
|
||||||
|
onPeriodModeChanged!(selection.first);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
constraints: const BoxConstraints(
|
if (heading != null) ...[
|
||||||
minWidth: 32,
|
const SizedBox(width: 8),
|
||||||
minHeight: 32,
|
Expanded(
|
||||||
),
|
child: Text(
|
||||||
padding: EdgeInsets.zero,
|
heading!,
|
||||||
icon: const Icon(Icons.chevron_left, size: 22),
|
maxLines: 1,
|
||||||
),
|
overflow: TextOverflow.ellipsis,
|
||||||
Expanded(
|
style: theme.textTheme.titleSmall?.copyWith(
|
||||||
child: Text(
|
fontWeight: FontWeight.w700,
|
||||||
periodTitle,
|
),
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
] else
|
||||||
IconButton(
|
const Spacer(),
|
||||||
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) ...[
|
if (showFilter) ...[
|
||||||
const SizedBox(width: 2),
|
const SizedBox(width: 4),
|
||||||
_filterButton(colors),
|
_filterButton(colors),
|
||||||
],
|
],
|
||||||
|
if (onEdit != null) ...[
|
||||||
|
const SizedBox(width: 2),
|
||||||
|
_editButton(colors, compact: true),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
else
|
const SizedBox(height: 4),
|
||||||
Row(
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(width: showFilter ? 36 : 0),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: _periodControls(theme, compact: true),
|
||||||
periodTitle,
|
),
|
||||||
textAlign: TextAlign.center,
|
if (hasLegend) ...[
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
const SizedBox(width: 8),
|
||||||
fontWeight: FontWeight.w700,
|
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)
|
const SizedBox(height: 8),
|
||||||
else
|
],
|
||||||
const SizedBox(width: 0),
|
if (period == OverviewPeriodMode.monthly)
|
||||||
],
|
Row(
|
||||||
),
|
children: [
|
||||||
if (hasLegend) ...[
|
IconButton(
|
||||||
Align(
|
tooltip: 'Previous year',
|
||||||
alignment: Alignment.centerRight,
|
onPressed: canGoBack ? onBack : null,
|
||||||
child: legend,
|
visualDensity: const VisualDensity(
|
||||||
),
|
horizontal: -4,
|
||||||
const SizedBox(height: 8),
|
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(
|
||||||
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,133 +1682,104 @@ class _FlowBars extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return BarChart(
|
return GestureDetector(
|
||||||
BarChartData(
|
behavior: HitTestBehavior.opaque,
|
||||||
maxY: safeMax,
|
onTap: onChartTap,
|
||||||
groupsSpace: period == OverviewPeriodMode.yearly ? 16 : 10,
|
child: IgnorePointer(
|
||||||
barTouchData: BarTouchData(
|
// fl_chart otherwise owns the gesture arena and empty taps go nowhere.
|
||||||
touchCallback: (event, response) {
|
// Tap opens this chart's filters; legend / history still open columns.
|
||||||
if (event is! FlTapUpEvent) return;
|
child: BarChart(
|
||||||
final spot = response?.spot;
|
BarChartData(
|
||||||
final index = spot?.touchedBarGroupIndex;
|
maxY: safeMax,
|
||||||
if (spot == null ||
|
groupsSpace: period == OverviewPeriodMode.yearly ? 16 : 10,
|
||||||
index == null ||
|
barTouchData: const BarTouchData(enabled: false),
|
||||||
index < 0 ||
|
titlesData: FlTitlesData(
|
||||||
index >= months.length) {
|
topTitles:
|
||||||
// Parent GestureDetector also handles empty-area taps.
|
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||||
return;
|
rightTitles:
|
||||||
}
|
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||||
final rodIndex = spot.touchedRodDataIndex;
|
leftTitles: AxisTitles(
|
||||||
// Rods: 0 = expense, 1 = income.
|
sideTitles: SideTitles(
|
||||||
final flow = rodIndex == 1
|
showTitles: true,
|
||||||
? TxFlowFilter.income
|
reservedSize: 42,
|
||||||
: TxFlowFilter.expense;
|
interval: safeMax / 3,
|
||||||
final scope = _ChartTapScope.maybeOf(context);
|
getTitlesWidget: (value, meta) {
|
||||||
if (scope != null) {
|
if (value <= 0) {
|
||||||
scope.openBar(() => onMonthTap(months[index], flow: flow));
|
return Text('0', style: theme.textTheme.labelSmall);
|
||||||
} else {
|
}
|
||||||
onMonthTap(months[index], flow: flow);
|
if ((value - safeMax).abs() < 0.01) {
|
||||||
}
|
return const SizedBox.shrink();
|
||||||
},
|
}
|
||||||
touchTooltipData: BarTouchTooltipData(
|
return Text(
|
||||||
getTooltipItem: (group, groupIndex, rod, rodIndex) {
|
_compactMoney(value),
|
||||||
final month = months[group.x.toInt()];
|
style: theme.textTheme.labelSmall,
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
BarChartRodData(
|
),
|
||||||
toY: monthly[_periodKey(months[i], period)]!.income,
|
bottomTitles: AxisTitles(
|
||||||
width: period == OverviewPeriodMode.yearly ? 14 : 9,
|
sideTitles: SideTitles(
|
||||||
borderRadius:
|
showTitles: true,
|
||||||
const BorderRadius.vertical(top: Radius.circular(6)),
|
reservedSize: 28,
|
||||||
color: _kIncomeColor,
|
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 axisInterval = ((safeMax - safeMin) / 3).abs();
|
||||||
final interval = axisInterval <= 0 ? 1.0 : axisInterval;
|
final interval = axisInterval <= 0 ? 1.0 : axisInterval;
|
||||||
|
|
||||||
return BarChart(
|
return GestureDetector(
|
||||||
BarChartData(
|
behavior: HitTestBehavior.opaque,
|
||||||
minY: safeMin,
|
onTap: onChartTap,
|
||||||
maxY: safeMax,
|
child: IgnorePointer(
|
||||||
groupsSpace: period == OverviewPeriodMode.yearly ? 16 : 10,
|
child: BarChart(
|
||||||
barTouchData: BarTouchData(
|
BarChartData(
|
||||||
touchCallback: (event, response) {
|
minY: safeMin,
|
||||||
if (event is! FlTapUpEvent) return;
|
maxY: safeMax,
|
||||||
final spot = response?.spot;
|
groupsSpace: period == OverviewPeriodMode.yearly ? 16 : 10,
|
||||||
final index = spot?.touchedBarGroupIndex;
|
barTouchData: const BarTouchData(enabled: false),
|
||||||
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:
|
||||||
@@ -2220,16 +2061,18 @@ class _LabelMonthBars extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
borderData: FlBorderData(show: false),
|
borderData: FlBorderData(show: false),
|
||||||
barGroups: [
|
barGroups: [
|
||||||
for (var i = 0; i < months.length; i++)
|
for (var i = 0; i < months.length; i++)
|
||||||
BarChartGroupData(
|
BarChartGroupData(
|
||||||
x: i,
|
x: i,
|
||||||
barsSpace: stacked ? 0 : 2,
|
barsSpace: stacked ? 0 : 2,
|
||||||
barRods: rodsFor(months[i]),
|
barRods: rodsFor(months[i]),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
duration: Duration.zero,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
duration: Duration.zero,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user