Files
tuiche/lib/pages/sleep_report/chart/HorizontalBarChart.dart
2025-08-14 15:53:22 +08:00

304 lines
8.7 KiB
Dart

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;
}