forked from eatvio/eatvio_app
feat(insights): first isights impemetation
This commit is contained in:
84
lib/features/insights/presentation/charts/bmi_chart.dart
Normal file
84
lib/features/insights/presentation/charts/bmi_chart.dart
Normal file
@@ -0,0 +1,84 @@
|
||||
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';
|
||||
|
||||
/// График ИМТ с цветными зонами: норма 18.5–25, избыточная масса 25–30, ожирение 30–40.
|
||||
class BmiChart extends StatelessWidget {
|
||||
const BmiChart({super.key, required this.model});
|
||||
|
||||
final BmiChartModel model;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final points = buildPoints(model.labels, model.values);
|
||||
if (points.isEmpty) {
|
||||
return InsightsChartPlaceholder(text: model.deltaText);
|
||||
}
|
||||
final s = S.of(context);
|
||||
final range = paddedRange(points.map((e) => e.value), minPadding: 2);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: insightsChartHeight,
|
||||
child: SfCartesianChart(
|
||||
key: const Key('insights_bmi_chart'),
|
||||
margin: EdgeInsets.zero,
|
||||
plotAreaBorderWidth: 0,
|
||||
primaryXAxis: buildInsightsCategoryAxis(),
|
||||
primaryYAxis: buildInsightsValueAxis(
|
||||
unit: s.insights_unit_bmi,
|
||||
min: range.min,
|
||||
max: range.max,
|
||||
plotBands: const [
|
||||
PlotBand(
|
||||
start: 18.5,
|
||||
end: 25,
|
||||
color: InsightsColors.bmiNormalZone,
|
||||
),
|
||||
PlotBand(
|
||||
start: 25,
|
||||
end: 30,
|
||||
color: InsightsColors.bmiOverweightZone,
|
||||
),
|
||||
PlotBand(
|
||||
start: 30,
|
||||
end: 40,
|
||||
color: InsightsColors.bmiObesityZone,
|
||||
),
|
||||
],
|
||||
),
|
||||
trackballBehavior: buildInsightsTrackball(),
|
||||
series: <CartesianSeries<InsightsPoint, String>>[
|
||||
SplineAreaSeries<InsightsPoint, String>(
|
||||
name: s.insights_tab_bmi,
|
||||
dataSource: points,
|
||||
xValueMapper: (point, _) => point.label,
|
||||
yValueMapper: (point, _) => point.value,
|
||||
borderColor: InsightsColors.blue,
|
||||
borderWidth: 3,
|
||||
gradient: buildInsightsAreaGradient(InsightsColors.blue),
|
||||
markerSettings: buildInsightsMarker(InsightsColors.blue),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (model.hint.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
model.hint,
|
||||
key: const Key('insights_bmi_hint'),
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: InsightsColors.subtitle,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
113
lib/features/insights/presentation/charts/daily_kbju_chart.dart
Normal file
113
lib/features/insights/presentation/charts/daily_kbju_chart.dart
Normal file
@@ -0,0 +1,113 @@
|
||||
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';
|
||||
|
||||
/// КБЖУ по дням: калории — площадь на левой оси («ккал»), Б/Ж/У — линии на
|
||||
/// правой оси («граммы»). [visibility] — Калории, Белки, Жиры, Углеводы.
|
||||
class DailyKbjuChart extends StatelessWidget {
|
||||
const DailyKbjuChart({
|
||||
super.key,
|
||||
required this.model,
|
||||
required this.visibility,
|
||||
});
|
||||
|
||||
final DailyChartModel model;
|
||||
final List<bool> visibility;
|
||||
|
||||
static const String _gramsAxisName = 'grams';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
final calories = buildPoints(model.labels, model.kcal);
|
||||
if (calories.isEmpty) {
|
||||
return InsightsChartPlaceholder(text: s.insights_not_enough);
|
||||
}
|
||||
final proteins = buildPoints(model.labels, model.proteins);
|
||||
final fats = buildPoints(model.labels, model.fats);
|
||||
final carbs = buildPoints(model.labels, model.carbs);
|
||||
|
||||
final grams = [
|
||||
(
|
||||
name: s.insights_series_proteins,
|
||||
color: InsightsColors.proteins,
|
||||
points: proteins,
|
||||
),
|
||||
(name: s.insights_series_fats, color: InsightsColors.fats, points: fats),
|
||||
(
|
||||
name: s.insights_series_carbs,
|
||||
color: InsightsColors.carbs,
|
||||
points: carbs,
|
||||
),
|
||||
];
|
||||
|
||||
return SizedBox(
|
||||
height: insightsChartHeight,
|
||||
child: SfCartesianChart(
|
||||
key: const Key('insights_daily_chart'),
|
||||
margin: EdgeInsets.zero,
|
||||
plotAreaBorderWidth: 0,
|
||||
primaryXAxis: buildInsightsCategoryAxis(),
|
||||
primaryYAxis: NumericAxis(
|
||||
title: AxisTitle(
|
||||
text: s.insights_unit_kcal,
|
||||
textStyle: insightsAxisLabelStyle,
|
||||
),
|
||||
labelStyle: insightsAxisLabelStyle,
|
||||
minimum: 0,
|
||||
majorTickLines: const MajorTickLines(size: 0),
|
||||
axisLine: const AxisLine(width: 0),
|
||||
majorGridLines: MajorGridLines(
|
||||
color: InsightsColors.border.withValues(alpha: 0.8),
|
||||
dashArray: const [4, 4],
|
||||
),
|
||||
),
|
||||
axes: <ChartAxis>[
|
||||
NumericAxis(
|
||||
name: _gramsAxisName,
|
||||
opposedPosition: true,
|
||||
minimum: 0,
|
||||
title: AxisTitle(
|
||||
text: s.insights_unit_grams,
|
||||
textStyle: insightsAxisLabelStyle,
|
||||
),
|
||||
labelStyle: insightsAxisLabelStyle,
|
||||
majorTickLines: const MajorTickLines(size: 0),
|
||||
axisLine: const AxisLine(width: 0),
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
),
|
||||
],
|
||||
trackballBehavior: buildInsightsTrackball(),
|
||||
series: <CartesianSeries<InsightsPoint, String>>[
|
||||
if (_isVisible(0))
|
||||
SplineAreaSeries<InsightsPoint, String>(
|
||||
name: s.insights_series_calories,
|
||||
dataSource: calories,
|
||||
xValueMapper: (point, _) => point.label,
|
||||
yValueMapper: (point, _) => point.value,
|
||||
borderColor: InsightsColors.calories,
|
||||
borderWidth: 3,
|
||||
gradient: buildInsightsAreaGradient(InsightsColors.calories),
|
||||
),
|
||||
for (var i = 0; i < grams.length; i++)
|
||||
if (_isVisible(i + 1))
|
||||
SplineSeries<InsightsPoint, String>(
|
||||
name: grams[i].name,
|
||||
yAxisName: _gramsAxisName,
|
||||
dataSource: grams[i].points,
|
||||
xValueMapper: (point, _) => point.label,
|
||||
yValueMapper: (point, _) => point.value,
|
||||
color: grams[i].color,
|
||||
width: 2.5,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool _isVisible(int index) =>
|
||||
index < visibility.length ? visibility[index] : true;
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:eatvio_app/features/insights/presentation/widgets/insights_colors.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncfusion_flutter_charts/charts.dart';
|
||||
|
||||
/// Точка графика: подпись оси X (`dd.MM`) и значение.
|
||||
class InsightsPoint {
|
||||
const InsightsPoint(this.label, this.value);
|
||||
|
||||
final String label;
|
||||
final double value;
|
||||
}
|
||||
|
||||
/// Склеивает `labels[]` и `values[]` (по минимальной длине).
|
||||
List<InsightsPoint> buildPoints(List<String> labels, List<double> values) {
|
||||
final count = math.min(labels.length, values.length);
|
||||
return [for (var i = 0; i < count; i++) InsightsPoint(labels[i], values[i])];
|
||||
}
|
||||
|
||||
/// Границы оси Y с отступом, чтобы линия не прижималась к краям (и ось не начиналась с нуля).
|
||||
({double min, double max}) paddedRange(
|
||||
Iterable<double> values, {
|
||||
double minPadding = 1,
|
||||
}) {
|
||||
if (values.isEmpty) {
|
||||
return (min: 0, max: 1);
|
||||
}
|
||||
final lowest = values.reduce(math.min);
|
||||
final highest = values.reduce(math.max);
|
||||
final padding = math.max((highest - lowest) * 0.2, minPadding);
|
||||
return (
|
||||
min: (lowest - padding).floorToDouble(),
|
||||
max: (highest + padding).ceilToDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
const TextStyle insightsAxisLabelStyle = TextStyle(
|
||||
fontSize: 11,
|
||||
color: InsightsColors.subtitle,
|
||||
);
|
||||
|
||||
CategoryAxis buildInsightsCategoryAxis() {
|
||||
return const CategoryAxis(
|
||||
arrangeByIndex: true,
|
||||
labelStyle: insightsAxisLabelStyle,
|
||||
majorGridLines: MajorGridLines(width: 0),
|
||||
majorTickLines: MajorTickLines(size: 0),
|
||||
axisLine: AxisLine(color: InsightsColors.border, width: 1),
|
||||
edgeLabelPlacement: EdgeLabelPlacement.shift,
|
||||
labelIntersectAction: AxisLabelIntersectAction.hide,
|
||||
);
|
||||
}
|
||||
|
||||
NumericAxis buildInsightsValueAxis({
|
||||
required String unit,
|
||||
required double min,
|
||||
required double max,
|
||||
List<PlotBand> plotBands = const [],
|
||||
}) {
|
||||
return NumericAxis(
|
||||
minimum: min,
|
||||
maximum: max,
|
||||
labelFormat: '{value} $unit',
|
||||
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],
|
||||
),
|
||||
plotBands: plotBands,
|
||||
);
|
||||
}
|
||||
|
||||
TrackballBehavior buildInsightsTrackball() {
|
||||
return TrackballBehavior(
|
||||
enable: true,
|
||||
activationMode: ActivationMode.singleTap,
|
||||
tooltipDisplayMode: TrackballDisplayMode.groupAllPoints,
|
||||
lineType: TrackballLineType.vertical,
|
||||
tooltipSettings: const InteractiveTooltip(),
|
||||
);
|
||||
}
|
||||
|
||||
MarkerSettings buildInsightsMarker(Color color) {
|
||||
return MarkerSettings(
|
||||
isVisible: true,
|
||||
width: 4,
|
||||
height: 4,
|
||||
color: Colors.white,
|
||||
borderColor: color,
|
||||
borderWidth: 2,
|
||||
);
|
||||
}
|
||||
|
||||
LinearGradient buildInsightsAreaGradient(Color color) {
|
||||
return LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [color.withValues(alpha: 0.18), color.withValues(alpha: 0)],
|
||||
);
|
||||
}
|
||||
|
||||
/// Пустое состояние графика (нет данных).
|
||||
class InsightsChartPlaceholder extends StatelessWidget {
|
||||
const InsightsChartPlaceholder({super.key, required this.text});
|
||||
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: insightsChartHeight,
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Text(
|
||||
text,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: InsightsColors.subtitle,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
210
lib/features/insights/presentation/charts/intra_day_chart.dart
Normal file
210
lib/features/insights/presentation/charts/intra_day_chart.dart
Normal file
@@ -0,0 +1,210 @@
|
||||
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],
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
86
lib/features/insights/presentation/charts/measure_chart.dart
Normal file
86
lib/features/insights/presentation/charts/measure_chart.dart
Normal file
@@ -0,0 +1,86 @@
|
||||
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';
|
||||
|
||||
/// Три линии: талия, грудь, бёдра. [visibility] — видимость серий в этом порядке.
|
||||
class MeasureChart extends StatelessWidget {
|
||||
const MeasureChart({
|
||||
super.key,
|
||||
required this.model,
|
||||
required this.visibility,
|
||||
required this.emptyText,
|
||||
});
|
||||
|
||||
final MeasureChartModel model;
|
||||
final List<bool> visibility;
|
||||
final String emptyText;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final waist = buildPoints(model.labels, model.waist);
|
||||
final chest = buildPoints(model.labels, model.chest);
|
||||
final hips = buildPoints(model.labels, model.hips);
|
||||
if (waist.isEmpty && chest.isEmpty && hips.isEmpty) {
|
||||
return InsightsChartPlaceholder(text: emptyText);
|
||||
}
|
||||
final s = S.of(context);
|
||||
final series = [
|
||||
(
|
||||
name: s.insights_series_waist,
|
||||
color: InsightsColors.waist,
|
||||
points: waist,
|
||||
),
|
||||
(
|
||||
name: s.insights_series_chest,
|
||||
color: InsightsColors.chest,
|
||||
points: chest,
|
||||
),
|
||||
(name: s.insights_series_hips, color: InsightsColors.hips, points: hips),
|
||||
];
|
||||
final visibleValues = <double>[
|
||||
for (var i = 0; i < series.length; i++)
|
||||
if (_isVisible(i)) ...series[i].points.map((e) => e.value),
|
||||
];
|
||||
final range = paddedRange(
|
||||
visibleValues.isEmpty
|
||||
? series.expand((e) => e.points).map((e) => e.value)
|
||||
: visibleValues,
|
||||
minPadding: 2,
|
||||
);
|
||||
|
||||
return SizedBox(
|
||||
height: insightsChartHeight,
|
||||
child: SfCartesianChart(
|
||||
key: const Key('insights_measure_chart'),
|
||||
margin: EdgeInsets.zero,
|
||||
plotAreaBorderWidth: 0,
|
||||
primaryXAxis: buildInsightsCategoryAxis(),
|
||||
primaryYAxis: buildInsightsValueAxis(
|
||||
unit: s.insights_unit_cm,
|
||||
min: range.min,
|
||||
max: range.max,
|
||||
),
|
||||
trackballBehavior: buildInsightsTrackball(),
|
||||
series: <CartesianSeries<InsightsPoint, String>>[
|
||||
for (var i = 0; i < series.length; i++)
|
||||
if (_isVisible(i))
|
||||
SplineSeries<InsightsPoint, String>(
|
||||
name: series[i].name,
|
||||
dataSource: series[i].points,
|
||||
xValueMapper: (point, _) => point.label,
|
||||
yValueMapper: (point, _) => point.value,
|
||||
color: series[i].color,
|
||||
width: 3,
|
||||
markerSettings: buildInsightsMarker(series[i].color),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool _isVisible(int index) =>
|
||||
index < visibility.length ? visibility[index] : true;
|
||||
}
|
||||
53
lib/features/insights/presentation/charts/weight_chart.dart
Normal file
53
lib/features/insights/presentation/charts/weight_chart.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
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';
|
||||
|
||||
class WeightChart extends StatelessWidget {
|
||||
const WeightChart({super.key, required this.model, required this.emptyText});
|
||||
|
||||
final WeightChartModel model;
|
||||
|
||||
/// Текст при отсутствии точек («Добавьте Ваш вес»).
|
||||
final String emptyText;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final points = buildPoints(model.labels, model.values);
|
||||
if (points.isEmpty) {
|
||||
return InsightsChartPlaceholder(text: emptyText);
|
||||
}
|
||||
final s = S.of(context);
|
||||
final range = paddedRange(points.map((e) => e.value));
|
||||
|
||||
return SizedBox(
|
||||
height: insightsChartHeight,
|
||||
child: SfCartesianChart(
|
||||
key: const Key('insights_weight_chart'),
|
||||
margin: EdgeInsets.zero,
|
||||
plotAreaBorderWidth: 0,
|
||||
primaryXAxis: buildInsightsCategoryAxis(),
|
||||
primaryYAxis: buildInsightsValueAxis(
|
||||
unit: s.insights_unit_kg,
|
||||
min: range.min,
|
||||
max: range.max,
|
||||
),
|
||||
trackballBehavior: buildInsightsTrackball(),
|
||||
series: <CartesianSeries<InsightsPoint, String>>[
|
||||
SplineAreaSeries<InsightsPoint, String>(
|
||||
name: s.insights_tab_weight,
|
||||
dataSource: points,
|
||||
xValueMapper: (point, _) => point.label,
|
||||
yValueMapper: (point, _) => point.value,
|
||||
borderColor: InsightsColors.blue,
|
||||
borderWidth: 3,
|
||||
gradient: buildInsightsAreaGradient(InsightsColors.blue),
|
||||
markerSettings: buildInsightsMarker(InsightsColors.blue),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user