add graph_view
This commit is contained in:
60
lib/controller/graph_controller.dart
Normal file
60
lib/controller/graph_controller.dart
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import 'package:flutter_tank_web_app/models/tank_model.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
class GraphController extends GetxController {
|
||||||
|
final listTankModel = <TankModel>[].obs;
|
||||||
|
final sumYearKm = 0.0.obs;
|
||||||
|
final sumYearLiters = 0.0.obs;
|
||||||
|
final sumYearPrice = 0.0.obs;
|
||||||
|
final averagePricePerLiter = 0.0.obs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
if (Get.arguments != null) {
|
||||||
|
var args = Get.arguments as List<TankModel>;
|
||||||
|
listTankModel.addAll(args);
|
||||||
|
_calcSumms();
|
||||||
|
}
|
||||||
|
super.onInit();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onReady() {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onClose() {}
|
||||||
|
|
||||||
|
// Calculate yearly summaries
|
||||||
|
void _calcSumms() {
|
||||||
|
if (listTankModel.length < 2) {
|
||||||
|
sumYearKm.value = 0.0;
|
||||||
|
sumYearLiters.value = 0.0;
|
||||||
|
sumYearPrice.value = 0.0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by date to calculate km correctly
|
||||||
|
listTankModel.sort((a, b) {
|
||||||
|
var dateA = DateTime.tryParse(a.szDate)!;
|
||||||
|
var dateB = DateTime.tryParse(b.szDate)!;
|
||||||
|
return dateA.compareTo(dateB);
|
||||||
|
});
|
||||||
|
|
||||||
|
var firstOdometer = double.tryParse(listTankModel.first.szOdometer) ?? 0.0;
|
||||||
|
var lastOdometer = double.tryParse(listTankModel.last.szOdometer) ?? 0.0;
|
||||||
|
|
||||||
|
sumYearKm.value = lastOdometer - firstOdometer;
|
||||||
|
|
||||||
|
double totalLiters = 0.0;
|
||||||
|
double totalPrice = 0.0;
|
||||||
|
|
||||||
|
for (var tank in listTankModel) {
|
||||||
|
totalLiters += double.tryParse(tank.szLiters) ?? 0.0;
|
||||||
|
totalPrice += double.tryParse(tank.szPriceTotal) ?? 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
sumYearLiters.value = totalLiters;
|
||||||
|
sumYearPrice.value = totalPrice;
|
||||||
|
averagePricePerLiter.value = totalPrice / totalLiters;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import '../models/tank_model.dart';
|
|||||||
import '../pages/detail_view.dart';
|
import '../pages/detail_view.dart';
|
||||||
import '../pages/edit_view.dart';
|
import '../pages/edit_view.dart';
|
||||||
import '../services/appwrite_service.dart';
|
import '../services/appwrite_service.dart';
|
||||||
|
import '../pages/graph_view.dart';
|
||||||
|
|
||||||
class HomeController extends GetxController {
|
class HomeController extends GetxController {
|
||||||
final isLoading = false.obs;
|
final isLoading = false.obs;
|
||||||
@@ -100,4 +101,8 @@ class HomeController extends GetxController {
|
|||||||
Future<void> navigateToAddTankEntry() async {
|
Future<void> navigateToAddTankEntry() async {
|
||||||
await Get.offAllNamed(EditPage.namedRoute);
|
await Get.offAllNamed(EditPage.namedRoute);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void goToGraphPage() {
|
||||||
|
Get.toNamed(GraphPage.namedRoute, arguments: listTankModel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:get/get.dart';
|
|||||||
|
|
||||||
import '../controller/detail_controller.dart';
|
import '../controller/detail_controller.dart';
|
||||||
import '../controller/edit_controller.dart';
|
import '../controller/edit_controller.dart';
|
||||||
|
import '../controller/graph_controller.dart';
|
||||||
import '../controller/home_controller.dart';
|
import '../controller/home_controller.dart';
|
||||||
import '../controller/login_controller.dart';
|
import '../controller/login_controller.dart';
|
||||||
import '../controller/signin_controller.dart';
|
import '../controller/signin_controller.dart';
|
||||||
@@ -17,6 +18,7 @@ class SampleBindings extends Bindings {
|
|||||||
Get.lazyPut<HomeController>(() => HomeController());
|
Get.lazyPut<HomeController>(() => HomeController());
|
||||||
Get.lazyPut<DetailController>(() => DetailController());
|
Get.lazyPut<DetailController>(() => DetailController());
|
||||||
Get.lazyPut<EditController>(() => EditController());
|
Get.lazyPut<EditController>(() => EditController());
|
||||||
|
Get.lazyPut<GraphController>(() => GraphController());
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import '../pages/detail_view.dart';
|
import '../pages/detail_view.dart';
|
||||||
import '../pages/edit_view.dart';
|
import '../pages/edit_view.dart';
|
||||||
|
import '../pages/graph_view.dart';
|
||||||
import 'sample_bindings.dart';
|
import 'sample_bindings.dart';
|
||||||
import '../pages/home_view.dart';
|
import '../pages/home_view.dart';
|
||||||
import '../pages/signin_view.dart';
|
import '../pages/signin_view.dart';
|
||||||
@@ -34,5 +35,10 @@ class SampleRouts {
|
|||||||
page: () => const EditPage(),
|
page: () => const EditPage(),
|
||||||
binding: sampleBindings,
|
binding: sampleBindings,
|
||||||
),
|
),
|
||||||
|
GetPage(
|
||||||
|
name: GraphPage.namedRoute,
|
||||||
|
page: () => const GraphPage(),
|
||||||
|
binding: sampleBindings,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
62
lib/models/chart_model.dart
Normal file
62
lib/models/chart_model.dart
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
class ChartDataPoint {
|
||||||
|
final int stopNumber;
|
||||||
|
final double litersPerStop;
|
||||||
|
final double pricePerStop;
|
||||||
|
final double pricePerLiter;
|
||||||
|
|
||||||
|
ChartDataPoint({
|
||||||
|
required this.stopNumber,
|
||||||
|
required this.litersPerStop,
|
||||||
|
required this.pricePerStop,
|
||||||
|
required this.pricePerLiter,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChartData {
|
||||||
|
final List<ChartDataPoint> dataPoints;
|
||||||
|
|
||||||
|
ChartData({required this.dataPoints});
|
||||||
|
|
||||||
|
factory ChartData.fromTankList(List<dynamic> tankList) {
|
||||||
|
List<ChartDataPoint> points = [];
|
||||||
|
|
||||||
|
for (int i = 0; i < tankList.length; i++) {
|
||||||
|
final tank = tankList[i];
|
||||||
|
final liters = double.tryParse(tank.szLiters) ?? 0.0;
|
||||||
|
final priceTotal = double.tryParse(tank.szPriceTotal) ?? 0.0;
|
||||||
|
final pricePerLiter = double.tryParse(tank.szPricePerLiter) ?? 0.0;
|
||||||
|
|
||||||
|
points.add(
|
||||||
|
ChartDataPoint(
|
||||||
|
stopNumber: i + 1,
|
||||||
|
litersPerStop: liters,
|
||||||
|
pricePerStop: priceTotal,
|
||||||
|
pricePerLiter: pricePerLiter,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ChartData(dataPoints: points);
|
||||||
|
}
|
||||||
|
|
||||||
|
double getMaxLiters() {
|
||||||
|
if (dataPoints.isEmpty) return 0;
|
||||||
|
return dataPoints
|
||||||
|
.map((p) => p.litersPerStop)
|
||||||
|
.reduce((a, b) => a > b ? a : b);
|
||||||
|
}
|
||||||
|
|
||||||
|
double getMaxPrice() {
|
||||||
|
if (dataPoints.isEmpty) return 0;
|
||||||
|
return dataPoints
|
||||||
|
.map((p) => p.pricePerStop)
|
||||||
|
.reduce((a, b) => a > b ? a : b);
|
||||||
|
}
|
||||||
|
|
||||||
|
double getMaxPricePerLiter() {
|
||||||
|
if (dataPoints.isEmpty) return 0;
|
||||||
|
return dataPoints
|
||||||
|
.map((p) => p.pricePerLiter)
|
||||||
|
.reduce((a, b) => a > b ? a : b);
|
||||||
|
}
|
||||||
|
}
|
||||||
0
lib/models/graph_model.dart
Normal file
0
lib/models/graph_model.dart
Normal file
103
lib/pages/graph_view.dart
Normal file
103
lib/pages/graph_view.dart
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import '../controller/graph_controller.dart';
|
||||||
|
import '../models/chart_model.dart';
|
||||||
|
import '../widgets/detail_info_card_widget.dart';
|
||||||
|
import '../widgets/detail_stat_widget.dart';
|
||||||
|
import '../widgets/chart_line_widget.dart';
|
||||||
|
|
||||||
|
class GraphPage extends GetView<GraphController> {
|
||||||
|
static const String namedRoute = '/graph-page';
|
||||||
|
|
||||||
|
const GraphPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var displayWidth = MediaQuery.of(context).size.width;
|
||||||
|
var displayHeight = MediaQuery.of(context).size.height;
|
||||||
|
var graphCtrl = controller;
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: Colors.blueGrey,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
title: const Text('Graph Page'),
|
||||||
|
centerTitle: true,
|
||||||
|
),
|
||||||
|
body: Container(
|
||||||
|
width: displayWidth,
|
||||||
|
height: displayHeight,
|
||||||
|
padding: EdgeInsets.all(16.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [
|
||||||
|
Colors.blueGrey[800]!,
|
||||||
|
Colors.blueGrey[600]!,
|
||||||
|
Colors.blueGrey[300]!,
|
||||||
|
Colors.blue[100]!,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Fahrzeugdaten
|
||||||
|
DetailInfoCardWidget(
|
||||||
|
title: 'Jahresstatistik',
|
||||||
|
children: [
|
||||||
|
DetailStatWidget(
|
||||||
|
icon: Icons.directions_car,
|
||||||
|
label: 'Jahreskilometerstand',
|
||||||
|
value: '${graphCtrl.sumYearKm.value} KM',
|
||||||
|
iconColor: Colors.blue,
|
||||||
|
valueSize: 24,
|
||||||
|
valueWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
DetailStatWidget(
|
||||||
|
icon: Icons.gas_meter,
|
||||||
|
label: 'Jahresliterverbrauch',
|
||||||
|
value:
|
||||||
|
'${graphCtrl.sumYearLiters.value.toStringAsFixed(2)} L',
|
||||||
|
iconColor: Colors.blue,
|
||||||
|
valueSize: 24,
|
||||||
|
valueWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
DetailStatWidget(
|
||||||
|
icon: Icons.euro,
|
||||||
|
label: 'Jahreskostenverbrauch',
|
||||||
|
value:
|
||||||
|
'${graphCtrl.sumYearPrice.value.toStringAsFixed(2)} €',
|
||||||
|
iconColor: Colors.blue,
|
||||||
|
valueSize: 24,
|
||||||
|
valueWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
DetailStatWidget(
|
||||||
|
icon: Icons.euro,
|
||||||
|
label: 'Jahresdurchschnittspreis pro Liter',
|
||||||
|
value:
|
||||||
|
'${graphCtrl.averagePricePerLiter.value.toStringAsFixed(2)} €',
|
||||||
|
iconColor: Colors.blue,
|
||||||
|
valueSize: 24,
|
||||||
|
valueWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Liniendiagramm
|
||||||
|
ChartLineWidget(
|
||||||
|
chartData: ChartData.fromTankList(graphCtrl.listTankModel),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,7 +48,13 @@ class HomePage extends GetView<HomeController> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.refresh),
|
icon: const Icon(Icons.refresh),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
controller.onInit();
|
homCtrl.onInit();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.collections),
|
||||||
|
onPressed: () {
|
||||||
|
homCtrl.goToGraphPage();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
|
|||||||
328
lib/widgets/chart_line_widget.dart
Normal file
328
lib/widgets/chart_line_widget.dart
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../models/chart_model.dart';
|
||||||
|
|
||||||
|
class ChartLineWidget extends StatelessWidget {
|
||||||
|
final ChartData chartData;
|
||||||
|
|
||||||
|
const ChartLineWidget({super.key, required this.chartData});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (chartData.dataPoints.isEmpty) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.blueGrey[50],
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: Colors.blueGrey[200]!),
|
||||||
|
),
|
||||||
|
child: const Center(child: Text('Keine Daten verfügbar')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final maxLiters = chartData.getMaxLiters();
|
||||||
|
final maxPrice = chartData.getMaxPrice();
|
||||||
|
final maxPricePerLiter = chartData.getMaxPricePerLiter();
|
||||||
|
|
||||||
|
final maxY = [
|
||||||
|
maxLiters,
|
||||||
|
maxPrice,
|
||||||
|
maxPricePerLiter,
|
||||||
|
].reduce((a, b) => a > b ? a : b);
|
||||||
|
final padding = maxY * 0.1;
|
||||||
|
final yAxisMax = maxY + padding;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.blueGrey[50],
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: Colors.blueGrey[200]!),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 300,
|
||||||
|
child: CustomPaint(
|
||||||
|
painter: LineChartPainter(
|
||||||
|
chartData: chartData,
|
||||||
|
yAxisMax: yAxisMax,
|
||||||
|
),
|
||||||
|
size: Size.infinite,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Wrap(
|
||||||
|
spacing: 20,
|
||||||
|
runSpacing: 10,
|
||||||
|
children: [
|
||||||
|
_LegendItem(color: Colors.red, label: 'Liter pro Stop'),
|
||||||
|
_LegendItem(color: Colors.green, label: 'Preis pro Stop (€)'),
|
||||||
|
_LegendItem(
|
||||||
|
color: Colors.amber,
|
||||||
|
label: 'Preis pro Liter (€)',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LegendItem extends StatelessWidget {
|
||||||
|
final Color color;
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
const _LegendItem({required this.color, required this.label});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(fontSize: 12, color: Colors.black87),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LineChartPainter extends CustomPainter {
|
||||||
|
final ChartData chartData;
|
||||||
|
final double yAxisMax;
|
||||||
|
|
||||||
|
LineChartPainter({required this.chartData, required this.yAxisMax});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
if (chartData.dataPoints.isEmpty) return;
|
||||||
|
|
||||||
|
final paint = Paint()
|
||||||
|
..color = Colors.blueGrey[300]!
|
||||||
|
..strokeWidth = 1;
|
||||||
|
|
||||||
|
final axisPaint = Paint()
|
||||||
|
..color = Colors.blueGrey[600]!
|
||||||
|
..strokeWidth = 2;
|
||||||
|
|
||||||
|
final padding = EdgeInsets.fromLTRB(40, 20, 20, 40);
|
||||||
|
final graphWidth = size.width - padding.left - padding.right;
|
||||||
|
final graphHeight = size.height - padding.top - padding.bottom;
|
||||||
|
|
||||||
|
// Draw grid and axes
|
||||||
|
_drawAxes(canvas, size, padding, axisPaint, paint);
|
||||||
|
|
||||||
|
// Draw lines
|
||||||
|
_drawLine(
|
||||||
|
canvas,
|
||||||
|
chartData,
|
||||||
|
padding,
|
||||||
|
graphWidth,
|
||||||
|
graphHeight,
|
||||||
|
Colors.red,
|
||||||
|
(point) => point.litersPerStop,
|
||||||
|
);
|
||||||
|
|
||||||
|
_drawLine(
|
||||||
|
canvas,
|
||||||
|
chartData,
|
||||||
|
padding,
|
||||||
|
graphWidth,
|
||||||
|
graphHeight,
|
||||||
|
Colors.green,
|
||||||
|
(point) => point.pricePerStop,
|
||||||
|
);
|
||||||
|
|
||||||
|
_drawLine(
|
||||||
|
canvas,
|
||||||
|
chartData,
|
||||||
|
padding,
|
||||||
|
graphWidth,
|
||||||
|
graphHeight,
|
||||||
|
Colors.amber,
|
||||||
|
(point) => point.pricePerLiter,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Draw points
|
||||||
|
final pointPaint = Paint()
|
||||||
|
..strokeWidth = 3
|
||||||
|
..strokeCap = StrokeCap.round;
|
||||||
|
|
||||||
|
final pointsSize = 4.0;
|
||||||
|
|
||||||
|
for (var point in chartData.dataPoints) {
|
||||||
|
final xPos =
|
||||||
|
padding.left +
|
||||||
|
(point.stopNumber - 1) *
|
||||||
|
(graphWidth /
|
||||||
|
(chartData.dataPoints.length - 1 > 0
|
||||||
|
? chartData.dataPoints.length - 1
|
||||||
|
: 1));
|
||||||
|
|
||||||
|
// Red points (Liters)
|
||||||
|
final yLiters =
|
||||||
|
padding.top +
|
||||||
|
graphHeight -
|
||||||
|
(point.litersPerStop / yAxisMax) * graphHeight;
|
||||||
|
pointPaint.color = Colors.red;
|
||||||
|
canvas.drawCircle(Offset(xPos, yLiters), pointsSize, pointPaint);
|
||||||
|
|
||||||
|
// Green points (Price)
|
||||||
|
final yPrice =
|
||||||
|
padding.top +
|
||||||
|
graphHeight -
|
||||||
|
(point.pricePerStop / yAxisMax) * graphHeight;
|
||||||
|
pointPaint.color = Colors.green;
|
||||||
|
canvas.drawCircle(Offset(xPos, yPrice), pointsSize, pointPaint);
|
||||||
|
|
||||||
|
// Amber points (Price per Liter)
|
||||||
|
final yPricePerLiter =
|
||||||
|
padding.top +
|
||||||
|
graphHeight -
|
||||||
|
(point.pricePerLiter / yAxisMax) * graphHeight;
|
||||||
|
pointPaint.color = Colors.amber;
|
||||||
|
canvas.drawCircle(Offset(xPos, yPricePerLiter), pointsSize, pointPaint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _drawAxes(
|
||||||
|
Canvas canvas,
|
||||||
|
Size size,
|
||||||
|
EdgeInsets padding,
|
||||||
|
Paint axisPaint,
|
||||||
|
Paint gridPaint,
|
||||||
|
) {
|
||||||
|
// Y-axis
|
||||||
|
canvas.drawLine(
|
||||||
|
Offset(padding.left, padding.top),
|
||||||
|
Offset(padding.left, size.height - padding.bottom),
|
||||||
|
axisPaint,
|
||||||
|
);
|
||||||
|
|
||||||
|
// X-axis
|
||||||
|
canvas.drawLine(
|
||||||
|
Offset(padding.left, size.height - padding.bottom),
|
||||||
|
Offset(size.width - padding.right, size.height - padding.bottom),
|
||||||
|
axisPaint,
|
||||||
|
);
|
||||||
|
|
||||||
|
final graphHeight = size.height - padding.top - padding.bottom;
|
||||||
|
final graphWidth = size.width - padding.left - padding.right;
|
||||||
|
|
||||||
|
// Y-axis labels
|
||||||
|
final steps = 5;
|
||||||
|
for (int i = 0; i <= steps; i++) {
|
||||||
|
final yPos = padding.top + (graphHeight / steps) * i;
|
||||||
|
final value = yAxisMax - (yAxisMax / steps) * i;
|
||||||
|
|
||||||
|
// Grid line
|
||||||
|
canvas.drawLine(
|
||||||
|
Offset(padding.left, yPos),
|
||||||
|
Offset(size.width - padding.right, yPos),
|
||||||
|
gridPaint..strokeWidth = 0.5,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Label
|
||||||
|
final textPainter = TextPainter(
|
||||||
|
text: TextSpan(
|
||||||
|
text: value.toStringAsFixed(1),
|
||||||
|
style: const TextStyle(color: Colors.black54, fontSize: 10),
|
||||||
|
),
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
);
|
||||||
|
textPainter.layout();
|
||||||
|
textPainter.paint(
|
||||||
|
canvas,
|
||||||
|
Offset(
|
||||||
|
padding.left - textPainter.width - 8,
|
||||||
|
yPos - textPainter.height / 2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// X-axis labels
|
||||||
|
for (int i = 0; i < chartData.dataPoints.length; i++) {
|
||||||
|
final xPos =
|
||||||
|
padding.left +
|
||||||
|
i *
|
||||||
|
(graphWidth /
|
||||||
|
(chartData.dataPoints.length - 1 > 0
|
||||||
|
? chartData.dataPoints.length - 1
|
||||||
|
: 1));
|
||||||
|
|
||||||
|
final textPainter = TextPainter(
|
||||||
|
text: TextSpan(
|
||||||
|
text: (i + 1).toString(),
|
||||||
|
style: const TextStyle(color: Colors.black54, fontSize: 10),
|
||||||
|
),
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
);
|
||||||
|
textPainter.layout();
|
||||||
|
textPainter.paint(
|
||||||
|
canvas,
|
||||||
|
Offset(xPos - textPainter.width / 2, size.height - padding.bottom + 8),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _drawLine(
|
||||||
|
Canvas canvas,
|
||||||
|
ChartData chartData,
|
||||||
|
EdgeInsets padding,
|
||||||
|
double graphWidth,
|
||||||
|
double graphHeight,
|
||||||
|
Color color,
|
||||||
|
double Function(ChartDataPoint) getValue,
|
||||||
|
) {
|
||||||
|
final paint = Paint()
|
||||||
|
..color = color
|
||||||
|
..strokeWidth = 2
|
||||||
|
..strokeCap = StrokeCap.round
|
||||||
|
..strokeJoin = StrokeJoin.round
|
||||||
|
..style = PaintingStyle.stroke;
|
||||||
|
|
||||||
|
final points = chartData.dataPoints;
|
||||||
|
if (points.length < 2) return;
|
||||||
|
|
||||||
|
final path = Path();
|
||||||
|
|
||||||
|
for (int i = 0; i < points.length; i++) {
|
||||||
|
final x =
|
||||||
|
padding.left +
|
||||||
|
i * (graphWidth / (points.length - 1 > 0 ? points.length - 1 : 1));
|
||||||
|
final y =
|
||||||
|
padding.top +
|
||||||
|
graphHeight -
|
||||||
|
(getValue(points[i]) / yAxisMax) * graphHeight;
|
||||||
|
|
||||||
|
if (i == 0) {
|
||||||
|
path.moveTo(x, y);
|
||||||
|
} else {
|
||||||
|
path.lineTo(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.drawPath(path, paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(LineChartPainter oldDelegate) {
|
||||||
|
return oldDelegate.chartData != chartData ||
|
||||||
|
oldDelegate.yAxisMax != yAxisMax;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user