forked from eatvio/eatvio_app
211 lines
7.0 KiB
Dart
211 lines
7.0 KiB
Dart
import 'package:eatvio_app/features/insights/models/insights_payload_model.dart';
|
||
import 'package:eatvio_app/features/insights/presentation/charts/insights_chart_common.dart';
|
||
import 'package:eatvio_app/features/insights/presentation/widgets/insights_colors.dart';
|
||
import 'package:eatvio_app/generated/l10n.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:syncfusion_flutter_charts/charts.dart';
|
||
|
||
/// График «В течение дня»: столбики Б/Ж/У (ккал) по приёмам пищи на оси 0–24 ч,
|
||
/// линии сытости и настроения (шкала 0–5) и затемнение времени сна.
|
||
///
|
||
/// [visibility] — Белки, Жиры, Углеводы, Сытость, Настроение.
|
||
class IntraDayChart extends StatelessWidget {
|
||
const IntraDayChart({
|
||
super.key,
|
||
required this.model,
|
||
required this.visibility,
|
||
required this.showSleep,
|
||
});
|
||
|
||
final IntraChartModel model;
|
||
final List<bool> visibility;
|
||
final bool showSleep;
|
||
|
||
static const String _scaleAxisName = 'scale';
|
||
static const double _barWidthPx = 26;
|
||
static const double _axesReservedWidthPx = 80;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final s = S.of(context);
|
||
if (model.bars.isEmpty && model.satiety.isEmpty && model.mood.isEmpty) {
|
||
return InsightsChartPlaceholder(text: s.insights_not_enough);
|
||
}
|
||
|
||
return SizedBox(
|
||
height: insightsChartHeight,
|
||
child: LayoutBuilder(
|
||
builder: (context, constraints) {
|
||
final plotWidth = (constraints.maxWidth - _axesReservedWidthPx).clamp(
|
||
120.0,
|
||
double.infinity,
|
||
);
|
||
final barFraction = (_barWidthPx / (plotWidth / 24)).clamp(0.1, 1.0);
|
||
return SfCartesianChart(
|
||
key: const Key('insights_intra_chart'),
|
||
margin: EdgeInsets.zero,
|
||
plotAreaBorderWidth: 0,
|
||
primaryXAxis: NumericAxis(
|
||
minimum: 0,
|
||
maximum: 24,
|
||
interval: 2,
|
||
labelStyle: insightsAxisLabelStyle,
|
||
majorGridLines: const MajorGridLines(width: 0),
|
||
majorTickLines: const MajorTickLines(size: 0),
|
||
axisLine: const AxisLine(color: InsightsColors.border, width: 1),
|
||
axisLabelFormatter: (details) => ChartAxisLabel(
|
||
'${details.value.toInt().toString().padLeft(2, '0')}:00',
|
||
details.textStyle,
|
||
),
|
||
plotBands: _sleepBands(),
|
||
),
|
||
primaryYAxis: NumericAxis(
|
||
minimum: 0,
|
||
title: AxisTitle(
|
||
text: s.insights_unit_kcal_per_meal,
|
||
textStyle: insightsAxisLabelStyle,
|
||
),
|
||
labelStyle: insightsAxisLabelStyle,
|
||
majorTickLines: const MajorTickLines(size: 0),
|
||
axisLine: const AxisLine(width: 0),
|
||
majorGridLines: MajorGridLines(
|
||
color: InsightsColors.border.withValues(alpha: 0.8),
|
||
dashArray: const [4, 4],
|
||
),
|
||
),
|
||
axes: const <ChartAxis>[
|
||
NumericAxis(
|
||
name: _scaleAxisName,
|
||
opposedPosition: true,
|
||
minimum: 0,
|
||
maximum: 5,
|
||
interval: 1,
|
||
labelStyle: insightsAxisLabelStyle,
|
||
majorTickLines: MajorTickLines(size: 0),
|
||
axisLine: AxisLine(width: 0),
|
||
majorGridLines: MajorGridLines(width: 0),
|
||
),
|
||
],
|
||
trackballBehavior: buildInsightsTrackball(),
|
||
series: <CartesianSeries<dynamic, double>>[
|
||
if (_isVisible(0))
|
||
_barSeries(
|
||
s.insights_series_proteins,
|
||
InsightsColors.proteins,
|
||
(bar) => bar.p,
|
||
barFraction,
|
||
),
|
||
if (_isVisible(1))
|
||
_barSeries(
|
||
s.insights_series_fats,
|
||
InsightsColors.fats,
|
||
(bar) => bar.f,
|
||
barFraction,
|
||
),
|
||
if (_isVisible(2))
|
||
_barSeries(
|
||
s.insights_series_carbs,
|
||
InsightsColors.carbs,
|
||
(bar) => bar.c,
|
||
barFraction,
|
||
borderRadius: const BorderRadius.vertical(
|
||
top: Radius.circular(6),
|
||
),
|
||
),
|
||
if (_isVisible(3) && model.satiety.isNotEmpty)
|
||
_scaleSeries(
|
||
s.insights_series_satiety,
|
||
InsightsColors.satiety,
|
||
model.satiety,
|
||
),
|
||
if (_isVisible(4) && model.mood.isNotEmpty)
|
||
_scaleSeries(
|
||
s.insights_series_mood,
|
||
InsightsColors.mood,
|
||
model.mood,
|
||
),
|
||
],
|
||
);
|
||
},
|
||
),
|
||
);
|
||
}
|
||
|
||
bool _isVisible(int index) =>
|
||
index < visibility.length ? visibility[index] : true;
|
||
|
||
CartesianSeries<IntraBarModel, double> _barSeries(
|
||
String name,
|
||
Color color,
|
||
double Function(IntraBarModel bar) valueOf,
|
||
double widthFraction, {
|
||
BorderRadius borderRadius = BorderRadius.zero,
|
||
}) {
|
||
return StackedColumnSeries<IntraBarModel, double>(
|
||
name: name,
|
||
dataSource: model.bars,
|
||
xValueMapper: (bar, _) => bar.x,
|
||
yValueMapper: (bar, _) => valueOf(bar),
|
||
color: color,
|
||
width: widthFraction,
|
||
spacing: 0,
|
||
borderRadius: borderRadius,
|
||
);
|
||
}
|
||
|
||
CartesianSeries<ScalePointModel, double> _scaleSeries(
|
||
String name,
|
||
Color color,
|
||
List<ScalePointModel> points,
|
||
) {
|
||
return SplineSeries<ScalePointModel, double>(
|
||
name: name,
|
||
yAxisName: _scaleAxisName,
|
||
dataSource: points,
|
||
xValueMapper: (point, _) => point.x,
|
||
yValueMapper: (point, _) => point.y,
|
||
splineType: SplineType.monotonic,
|
||
color: color,
|
||
width: 2.5,
|
||
markerSettings: MarkerSettings(
|
||
isVisible: true,
|
||
width: 3,
|
||
height: 3,
|
||
color: color,
|
||
borderColor: color,
|
||
),
|
||
);
|
||
}
|
||
|
||
/// Затемнение `[0, wake]` и `[sleep, 24]` плюс пунктиры подъёма и отхода ко сну.
|
||
List<PlotBand> _sleepBands() {
|
||
if (!showSleep) {
|
||
return const [];
|
||
}
|
||
final wake = model.wake;
|
||
final sleep = model.sleep;
|
||
return [
|
||
if (wake != null && wake > 0)
|
||
PlotBand(start: 0, end: wake, color: InsightsColors.sleepShade),
|
||
if (sleep != null && sleep < 24 && (wake == null || sleep > wake))
|
||
PlotBand(start: sleep, end: 24, color: InsightsColors.sleepShade),
|
||
if (wake != null)
|
||
PlotBand(
|
||
start: wake,
|
||
end: wake,
|
||
borderColor: InsightsColors.wake,
|
||
borderWidth: 1.5,
|
||
dashArray: const [5, 4],
|
||
),
|
||
if (sleep != null)
|
||
PlotBand(
|
||
start: sleep,
|
||
end: sleep,
|
||
borderColor: InsightsColors.sleepLine,
|
||
borderWidth: 1.5,
|
||
dashArray: const [5, 4],
|
||
),
|
||
];
|
||
}
|
||
}
|