Files
tuiche/lib/pages/sleep_report/chart/HorizontalBarChart.dart
2026-03-10 12:01:00 +08:00

662 lines
20 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// import 'package:flutter/material.dart';
// import 'package:flutter_svg/flutter_svg.dart';
// import 'package:vbvs_app/common/color/appConstants.dart';
// import 'package:vbvs_app/common/util/FitTool.dart';
// import 'package:vbvs_app/common/util/MyUtils.dart';
// import 'package:vbvs_app/component/tool/ClickableContainer.dart';
// import 'package:vbvs_app/enum/APPPackageType.dart';
// import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart';
// class HorizontalBarChart extends StatelessWidget {
// final List<Map<String, dynamic>> showLabel;
// final bool showPercent;
// const HorizontalBarChart({
// super.key,
// required this.showLabel,
// this.showPercent = true,
// });
// @override
// Widget build(BuildContext context) {
// final data = showLabel.map((item) {
// return BarData(
// label: item['name'],
// value: (item['percent'] ?? 0).toDouble(),
// color: item['color'] ?? Colors.grey,
// explain: item['explain'] ?? '',
// );
// }).toList();
// final double labelWidth = (MediaQuery.of(context).size.width * 0.5).clamp(
// MediaQuery.of(context).size.width * 0.08,
// MediaQuery.of(context).size.width * 0.22);
// final double barHeight = 24.0.rpx;
// final double barSpacing = 40.0.rpx;
// final totalHeight = data.length * (barHeight + barSpacing) + 30.rpx;
// return SizedBox(
// height: totalHeight,
// child: Row(
// children: [
// // 左侧标签列
// SizedBox(
// width: labelWidth,
// child: ListView.builder(
// physics: const NeverScrollableScrollPhysics(),
// itemCount: data.length,
// itemBuilder: (context, index) {
// final bar = data[index];
// return Container(
// height: barHeight + barSpacing,
// alignment: Alignment.centerRight,
// child: LabelWithSvg(
// label: bar.label,
// explain: bar.explain,
// ),
// );
// },
// ),
// ),
// SizedBox(
// width: 16.rpx,
// ),
// // 右侧柱状图区
// Expanded(
// child: Stack(
// children: [
// // 网格线背景层
// CustomPaint(
// size: Size(double.infinity, totalHeight),
// painter: GridPainter(totalHeight: totalHeight),
// ),
// // 柱状图列表
// ListView.builder(
// physics: const NeverScrollableScrollPhysics(),
// itemCount: data.length,
// itemBuilder: (context, index) {
// final bar = data[index];
// return SizedBox(
// height: barHeight + barSpacing,
// child: CustomPaint(
// painter: SingleBarPainter(
// value: bar.value,
// color: bar.color,
// showPercent: showPercent,
// ),
// ),
// );
// },
// )
// ],
// ),
// ),
// ],
// ),
// );
// }
// }
// class BarData {
// final String label;
// final double value;
// final Color color;
// final String explain;
// BarData({
// required this.label,
// required this.value,
// required this.color,
// required this.explain,
// });
// }
// class LabelWithSvg extends StatelessWidget {
// final String label;
// final String explain;
// const LabelWithSvg({
// super.key,
// required this.label,
// required this.explain,
// });
// @override
// Widget build(BuildContext context) {
// final textStyle =
// TextStyle(color: themeController.currentColor.sc3, fontSize: 26.rpx);
// return Row(
// mainAxisAlignment: MainAxisAlignment.end,
// crossAxisAlignment: CrossAxisAlignment.center,
// children: [
// Flexible(
// child: Text(
// label,
// style: textStyle,
// overflow: TextOverflow.ellipsis,
// ),
// ),
// // SizedBox(width: 8.rpx),
// ClickableContainer(
// backgroundColor: Colors.transparent,
// highlightColor: Colors.white,
// padding:
// EdgeInsetsDirectional.fromSTEB(14.rpx, 14.rpx, 14.rpx, 14.rpx),
// borderRadius: 0.rpx,
// onTap: () {
// // Get.toNamed("/deviceShareListPage", arguments: explain);
// if (AppConstants().ent_type == APPPackageType.MHT.code) {
// showTipDialog(
// context,
// Container(
// child: Text(
// explain,
// style: TextStyle(fontSize: 26.rpx, color: Colors.black),
// ),
// ),
// backgroundColor: Color(0xFFFFFFFF),
// colors: [
// Color(0XFF1592AA),
// Color(0xFF0C83A7),
// Color(0xFF006FA3)
// ],
// );
// } else {
// showTipDialog(
// context,
// Container(
// child: Text(
// explain,
// style: TextStyle(
// fontSize: 26.rpx,
// color: themeController.currentColor.sc3),
// ),
// ),
// backgroundColor: themeController.currentColor.sc17,
// colors: AppConstants().thNormalButton,
// );
// }
// },
// child: SizedBox(
// width: 17.rpx,
// height: 17.rpx,
// child: SvgPicture.asset(
// 'assets/img/icon/explain.svg',
// fit: BoxFit.cover,
// color: Colors.white,
// ),
// ),
// ),
// ],
// );
// }
// }
// class SingleBarPainter extends CustomPainter {
// final double value;
// final Color color;
// final bool showPercent;
// final double maxValue = 100;
// SingleBarPainter({
// required this.value,
// required this.color,
// required this.showPercent,
// });
// @override
// void paint(Canvas canvas, Size size) {
// final barHeight = 24.0.rpx;
// final rightPadding = 20.0.rpx;
// final chartWidth = size.width - rightPadding;
// final left = 0.0;
// final right = left + (value / maxValue) * chartWidth;
// final rect = Rect.fromLTWH(
// left, (size.height - barHeight) / 2, right - left, barHeight);
// final paint = Paint()..color = color;
// canvas.drawRect(rect, paint);
// if (showPercent) {
// final textStyle =
// TextStyle(color: themeController.currentColor.sc3, fontSize: 26.rpx);
// final textPainter = TextPainter(textDirection: TextDirection.ltr);
// textPainter.text =
// TextSpan(text: '${value.toStringAsFixed(0)}', style: textStyle);
// textPainter.layout();
// canvas.save();
// canvas.clipRect(Rect.fromLTWH(
// left, (size.height - barHeight) / 2, chartWidth, barHeight));
// textPainter.paint(
// canvas,
// Offset(right + 4.0.rpx, (size.height - textPainter.height) / 2),
// );
// canvas.restore();
// }
// }
// @override
// bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
// }
// class GridPainter extends CustomPainter {
// final double totalHeight;
// final int gridCount = 5;
// final double maxValue = 100;
// final double bottomPadding = 30.0.rpx;
// GridPainter({required this.totalHeight});
// @override
// void paint(Canvas canvas, Size size) {
// final Paint gridPaint = Paint()
// ..color = Colors.grey.withOpacity(0.3)
// ..strokeWidth = 1.0.rpx;
// final double rightPadding = 20.0.rpx;
// final double chartWidth = size.width - rightPadding;
// final double chartHeight = totalHeight - bottomPadding;
// for (int i = 0; i <= gridCount; i++) {
// double dx = (i / gridCount) * chartWidth;
// _drawDashedLine(
// canvas, Offset(dx, 0), Offset(dx, chartHeight), gridPaint);
// final percent = (i / gridCount) * maxValue;
// final TextPainter textPainter = TextPainter(
// textDirection: TextDirection.ltr,
// text: TextSpan(
// text: '${percent.toInt()}',
// style: TextStyle(color: Colors.grey, fontSize: 18.rpx),
// ),
// );
// textPainter.layout();
// textPainter.paint(
// canvas, Offset(dx - textPainter.width / 2, chartHeight + 4.0.rpx));
// }
// }
// void _drawDashedLine(Canvas canvas, Offset start, Offset end, Paint paint) {
// const double dashWidth = 6.0;
// const double dashSpace = 4.0;
// final double totalHeight = (end.dy - start.dy).abs();
// double currentY = start.dy;
// while (currentY < end.dy) {
// final double nextY = currentY + dashWidth;
// canvas.drawLine(
// Offset(start.dx, currentY),
// Offset(start.dx, nextY > end.dy ? end.dy : nextY),
// paint,
// );
// currentY = nextY + dashSpace;
// }
// }
// @override
// bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
// }
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:vbvs_app/common/color/appConstants.dart';
import 'package:vbvs_app/common/util/FitTool.dart';
import 'package:vbvs_app/common/util/MyUtils.dart';
import 'package:vbvs_app/component/tool/ClickableContainer.dart';
import 'package:vbvs_app/enum/APPPackageType.dart';
import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart';
class HorizontalBarChart extends StatelessWidget {
final List<Map<String, dynamic>> showLabel;
final bool showPercent;
final bool showRangeBackground; // 新增参数,控制是否显示区间背景
const HorizontalBarChart({
super.key,
required this.showLabel,
this.showPercent = true,
this.showRangeBackground = false, // 默认为false
});
@override
Widget build(BuildContext context) {
final data = showLabel.map((item) {
return BarData(
label: item['name'],
value: (item['percent'] ?? 0).toDouble(),
color: item['color'] ?? Colors.grey,
explain: item['explain'] ?? '',
);
}).toList();
final double labelWidth = (MediaQuery.of(context).size.width * 0.5).clamp(
MediaQuery.of(context).size.width * 0.08,
MediaQuery.of(context).size.width * 0.22);
final double barHeight = 24.0.rpx;
final double barSpacing = 40.0.rpx;
final totalHeight = data.length * (barHeight + barSpacing) + 30.rpx;
return SizedBox(
height: totalHeight,
child: Row(
children: [
// 左侧标签列
SizedBox(
width: labelWidth,
child: ListView.builder(
physics: const NeverScrollableScrollPhysics(),
itemCount: data.length,
itemBuilder: (context, index) {
final bar = data[index];
return Container(
height: barHeight + barSpacing,
alignment: Alignment.centerRight,
child: LabelWithSvg(
label: bar.label,
explain: bar.explain,
),
);
},
),
),
SizedBox(
width: 16.rpx,
),
// 右侧柱状图区
Expanded(
child: Stack(
children: [
// 背景层 - 包含网格线和区间背景
CustomPaint(
size: Size(double.infinity, totalHeight),
painter: GridPainter(
totalHeight: totalHeight,
showRangeBackground: showRangeBackground,
),
),
// 柱状图列表
ListView.builder(
physics: const NeverScrollableScrollPhysics(),
itemCount: data.length,
itemBuilder: (context, index) {
final bar = data[index];
return SizedBox(
height: barHeight + barSpacing,
child: CustomPaint(
painter: SingleBarPainter(
value: bar.value,
color: bar.color,
showPercent: showPercent,
),
),
);
},
)
],
),
),
],
),
);
}
}
class BarData {
final String label;
final double value;
final Color color;
final String explain;
BarData({
required this.label,
required this.value,
required this.color,
required this.explain,
});
}
class LabelWithSvg extends StatelessWidget {
final String label;
final String explain;
const LabelWithSvg({
super.key,
required this.label,
required this.explain,
});
@override
Widget build(BuildContext context) {
final textStyle =
TextStyle(color: themeController.currentColor.sc3, fontSize: 26.rpx);
return Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Flexible(
child: Text(
label,
style: textStyle,
overflow: TextOverflow.ellipsis,
),
),
// SizedBox(width: 8.rpx),
ClickableContainer(
backgroundColor: Colors.transparent,
highlightColor: Colors.white,
padding:
EdgeInsetsDirectional.fromSTEB(14.rpx, 14.rpx, 14.rpx, 14.rpx),
borderRadius: 0.rpx,
onTap: () {
// Get.toNamed("/deviceShareListPage", arguments: explain);
if (AppConstants().ent_type == APPPackageType.MHT.code) {
showTipDialog(
context,
Container(
child: Text(
explain,
style: TextStyle(fontSize: 26.rpx, color: Colors.black),
),
),
backgroundColor: Color(0xFFFFFFFF),
colors: [
Color(0XFF1592AA),
Color(0xFF0C83A7),
Color(0xFF006FA3)
],
);
} else {
showTipDialog(
context,
Container(
child: Text(
explain,
style: TextStyle(
fontSize: 26.rpx,
color: themeController.currentColor.sc3),
),
),
backgroundColor: themeController.currentColor.sc17,
colors: AppConstants().thNormalButton,
);
}
},
child: SizedBox(
width: 17.rpx,
height: 17.rpx,
child: SvgPicture.asset(
'assets/img/icon/explain.svg',
fit: BoxFit.cover,
color: Colors.white,
),
),
),
],
);
}
}
class SingleBarPainter extends CustomPainter {
final double value;
final Color color;
final bool showPercent;
final double maxValue = 100;
SingleBarPainter({
required this.value,
required this.color,
required this.showPercent,
});
@override
void paint(Canvas canvas, Size size) {
final barHeight = 24.0.rpx;
final rightPadding = 20.0.rpx;
final chartWidth = size.width - rightPadding;
final left = 0.0;
final right = left + (value / maxValue) * chartWidth;
final rect = Rect.fromLTWH(
left, (size.height - barHeight) / 2, right - left, barHeight);
final paint = Paint()..color = color;
canvas.drawRect(rect, paint);
if (showPercent) {
final textStyle =
TextStyle(color: themeController.currentColor.sc3, fontSize: 26.rpx);
final textPainter = TextPainter(textDirection: TextDirection.ltr);
textPainter.text =
TextSpan(text: '${value.toStringAsFixed(0)}', style: textStyle);
textPainter.layout();
canvas.save();
canvas.clipRect(Rect.fromLTWH(
left, (size.height - barHeight) / 2, chartWidth, barHeight));
textPainter.paint(
canvas,
Offset(right + 4.0.rpx, (size.height - textPainter.height) / 2),
);
canvas.restore();
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
if (oldDelegate is SingleBarPainter) {
return oldDelegate.value != value || oldDelegate.color != color;
}
return true;
}
}
class GridPainter extends CustomPainter {
final double totalHeight;
final int gridCount = 5;
final double maxValue = 100;
final double bottomPadding = 30.0.rpx;
final bool showRangeBackground; // 新增参数
GridPainter({
required this.totalHeight,
this.showRangeBackground = false,
});
@override
void paint(Canvas canvas, Size size) {
final double rightPadding = 20.0.rpx;
final double chartWidth = size.width - rightPadding;
final double chartHeight = totalHeight - bottomPadding;
// 如果需要显示区间背景
if (showRangeBackground) {
final double segmentWidth = chartWidth / gridCount;
// 计算0-60对应的网格区间60对应3格因为每格20
// 0-60: 第0-3格0,20,40,60
for (int i = 0; i < 3; i++) {
final rect = Rect.fromLTWH(
i * segmentWidth,
0,
segmentWidth,
chartHeight,
);
final paint = Paint()
..color = themeController.currentColor.sc1.withOpacity(0.2)
..style = PaintingStyle.fill;
canvas.drawRect(rect, paint);
}
// 60-100: 第3-5格60,80,100
for (int i = 3; i < 5; i++) {
final rect = Rect.fromLTWH(
i * segmentWidth,
0,
segmentWidth,
chartHeight,
);
final paint = Paint()
..color = themeController.currentColor.sc9.withOpacity(0.2)
..style = PaintingStyle.fill;
canvas.drawRect(rect, paint);
}
}
// 绘制网格线
final Paint gridPaint = Paint()
..color = Colors.grey.withOpacity(0.3)
..strokeWidth = 1.0.rpx;
for (int i = 0; i <= gridCount; i++) {
double dx = (i / gridCount) * chartWidth;
_drawDashedLine(
canvas, Offset(dx, 0), Offset(dx, chartHeight), gridPaint);
final percent = (i / gridCount) * maxValue;
final TextPainter textPainter = TextPainter(
textDirection: TextDirection.ltr,
text: TextSpan(
text: '${percent.toInt()}',
style: TextStyle(color: Colors.grey, fontSize: 18.rpx),
),
);
textPainter.layout();
textPainter.paint(
canvas, Offset(dx - textPainter.width / 2, chartHeight + 4.0.rpx));
}
}
void _drawDashedLine(Canvas canvas, Offset start, Offset end, Paint paint) {
const double dashWidth = 6.0;
const double dashSpace = 4.0;
final double totalHeight = (end.dy - start.dy).abs();
double currentY = start.dy;
while (currentY < end.dy) {
final double nextY = currentY + dashWidth;
canvas.drawLine(
Offset(start.dx, currentY),
Offset(start.dx, nextY > end.dy ? end.dy : nextY),
paint,
);
currentY = nextY + dashSpace;
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
if (oldDelegate is GridPainter) {
return oldDelegate.showRangeBackground != showRangeBackground;
}
return true;
}
}