fix(app): подписи осей и легенда графика стоимости
Крайние подписи оси Y (414.6K поверх 400K) скрыты, числа в русской краткой форме, подписи оси X не обрезаются справа. Добавлена легенда: сплошная линия — стоимость, пунктир — вложено нетто.
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
/// «12,3 тыс», «1,2 млн» — for axis labels and cells too narrow for a full amount.
|
||||||
|
///
|
||||||
|
/// fl_chart's own short form («414.6K») is English and rounds a step of 50 000 to the same
|
||||||
|
/// label twice; this one keeps the Russian suffixes and one decimal below a hundred.
|
||||||
|
String compactNumber(double value) {
|
||||||
|
final n = value.abs();
|
||||||
|
String scaled(double x, String suffix) {
|
||||||
|
final s = x >= 100
|
||||||
|
? x.round().toString()
|
||||||
|
: x.toStringAsFixed(1).replaceAll(RegExp(r'\.0$'), '');
|
||||||
|
return '${s.replaceAll('.', ',')} $suffix';
|
||||||
|
}
|
||||||
|
|
||||||
|
final sign = value < 0 ? '−' : '';
|
||||||
|
if (n >= 1e6) return '$sign${scaled(n / 1e6, 'млн')}';
|
||||||
|
if (n >= 1e3) return '$sign${scaled(n / 1e3, 'тыс')}';
|
||||||
|
return '$sign${n.round()}';
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../../core/theme/chart_colors.dart';
|
import '../../core/theme/chart_colors.dart';
|
||||||
|
import '../../core/utils/compact_number.dart';
|
||||||
import '../../core/utils/ru_date.dart';
|
import '../../core/utils/ru_date.dart';
|
||||||
import '../../core/widgets/async_value_view.dart';
|
import '../../core/widgets/async_value_view.dart';
|
||||||
import '../../core/widgets/empty_state.dart';
|
import '../../core/widgets/empty_state.dart';
|
||||||
@@ -249,7 +250,8 @@ class _ValueChart extends StatelessWidget {
|
|||||||
spots.add(FlSpot(i.toDouble(), _d(rows[i].totalRub)));
|
spots.add(FlSpot(i.toDouble(), _d(rows[i].totalRub)));
|
||||||
invested.add(FlSpot(i.toDouble(), _d(rows[i].investedNetRub)));
|
invested.add(FlSpot(i.toDouble(), _d(rows[i].investedNetRub)));
|
||||||
}
|
}
|
||||||
return SizedBox(
|
const names = ['Стоимость', 'Вложено (нетто)'];
|
||||||
|
final chart = SizedBox(
|
||||||
height: 220,
|
height: 220,
|
||||||
child: LineChart(
|
child: LineChart(
|
||||||
LineChartData(
|
LineChartData(
|
||||||
@@ -258,20 +260,47 @@ class _ValueChart extends StatelessWidget {
|
|||||||
titlesData: FlTitlesData(
|
titlesData: FlTitlesData(
|
||||||
topTitles: const AxisTitles(),
|
topTitles: const AxisTitles(),
|
||||||
rightTitles: const AxisTitles(),
|
rightTitles: const AxisTitles(),
|
||||||
leftTitles: const AxisTitles(
|
leftTitles: AxisTitles(
|
||||||
sideTitles: SideTitles(showTitles: true, reservedSize: 56),
|
sideTitles: SideTitles(
|
||||||
|
showTitles: true,
|
||||||
|
reservedSize: 56,
|
||||||
|
getTitlesWidget: (value, meta) {
|
||||||
|
// the axis ends sit on the data's min/max and land on top of the round
|
||||||
|
// labels next to them (414,6 тыс over 400 тыс)
|
||||||
|
if (value == meta.min || value == meta.max) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
return SideTitleWidget(
|
||||||
|
axisSide: meta.axisSide,
|
||||||
|
child: Text(
|
||||||
|
compactNumber(value),
|
||||||
|
style: theme.textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
bottomTitles: AxisTitles(
|
bottomTitles: AxisTitles(
|
||||||
sideTitles: SideTitles(
|
sideTitles: SideTitles(
|
||||||
showTitles: true,
|
showTitles: true,
|
||||||
reservedSize: 28,
|
reservedSize: 28,
|
||||||
interval: (rows.length / 4).clamp(1, double.infinity),
|
// ticks at the quarters of the span, so the last one is the last day
|
||||||
|
interval: ((rows.length - 1) / 4).clamp(1.0, double.infinity),
|
||||||
getTitlesWidget: (value, meta) {
|
getTitlesWidget: (value, meta) {
|
||||||
|
// float steps can land a label a hair before the last one; keep only one
|
||||||
|
if (value != meta.max &&
|
||||||
|
meta.max - value < meta.appliedInterval * 0.1) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
final i = value.round();
|
final i = value.round();
|
||||||
if (i < 0 || i >= rows.length) return const SizedBox.shrink();
|
if (i < 0 || i >= rows.length) return const SizedBox.shrink();
|
||||||
return Text(
|
return SideTitleWidget(
|
||||||
|
axisSide: meta.axisSide,
|
||||||
|
fitInside: SideTitleFitInsideData.fromTitleMeta(meta),
|
||||||
|
child: Text(
|
||||||
ruMonthYearShort(rows[i].d),
|
ruMonthYearShort(rows[i].d),
|
||||||
style: theme.textTheme.bodySmall,
|
style: theme.textTheme.bodySmall,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -297,9 +326,10 @@ class _ValueChart extends StatelessWidget {
|
|||||||
lineTouchData: LineTouchData(
|
lineTouchData: LineTouchData(
|
||||||
touchTooltipData: LineTouchTooltipData(
|
touchTooltipData: LineTouchTooltipData(
|
||||||
getTooltipItems: (touched) => [
|
getTooltipItems: (touched) => [
|
||||||
for (final t in touched)
|
for (final (i, t) in touched.indexed)
|
||||||
LineTooltipItem(
|
LineTooltipItem(
|
||||||
'${ruDate(rows[t.x.round()].d)}\n'
|
'${i == 0 ? '${ruDate(rows[t.x.round()].d)}\n' : ''}'
|
||||||
|
'${names[t.barIndex]}: '
|
||||||
'${MoneyText.format(t.y.toStringAsFixed(2), 'RUB')}',
|
'${MoneyText.format(t.y.toStringAsFixed(2), 'RUB')}',
|
||||||
theme.textTheme.bodySmall ?? const TextStyle(),
|
theme.textTheme.bodySmall ?? const TextStyle(),
|
||||||
),
|
),
|
||||||
@@ -309,6 +339,43 @@ class _ValueChart extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// the dashed line has no other explanation: say what each of the two is
|
||||||
|
Widget key(Color color, String label, {bool dashed = false}) => Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 18,
|
||||||
|
child: dashed
|
||||||
|
? Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
for (var i = 0; i < 3; i++)
|
||||||
|
Container(width: 4, height: 2, color: color),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: Container(height: 2, color: color),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(label, style: theme.textTheme.bodySmall),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
chart,
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Wrap(
|
||||||
|
spacing: 16,
|
||||||
|
runSpacing: 4,
|
||||||
|
children: [
|
||||||
|
key(ChartColors.slot1Blue, names[0]),
|
||||||
|
key(ChartColors.slot4Yellow, names[1], dashed: true),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user