chart.dart 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. import 'dart:math';
  2. import 'dart:ui' as ui;
  3. import 'dart:ui';
  4. import 'package:flutter/cupertino.dart';
  5. import 'package:flutter/material.dart';
  6. import 'package:sport/services/Converter.dart';
  7. const WEEK = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
  8. class ChartItem {
  9. final String createdAt;
  10. final num value;
  11. ChartItem(this.createdAt, this.value);
  12. }
  13. class Chart extends CustomPainter {
  14. final Paint _paint = Paint()
  15. ..color = const Color(0xffDCDCDC)
  16. ..strokeWidth = 0.5
  17. ..isAntiAlias = true;
  18. final Paint _paintCurrent = Paint()
  19. ..color = const Color(0xffFFC400)
  20. ..strokeWidth = 0.5
  21. ..isAntiAlias = true;
  22. static const Color _maxColor = Color(0xffFF5B1D);
  23. final Paint _maxPaint = Paint()
  24. ..color = _maxColor
  25. ..strokeWidth = 0.5
  26. ..isAntiAlias = true;
  27. static const double fontSize = 12;
  28. final ParagraphStyle _labelStyle = ParagraphStyle(
  29. textAlign: TextAlign.left,
  30. fontSize: fontSize,
  31. );
  32. final ParagraphStyle _valueStyle = ParagraphStyle(
  33. textAlign: TextAlign.right,
  34. fontSize: fontSize,
  35. );
  36. final Paint columnPaint = Paint()
  37. ..color = const Color(0xffFFC400)
  38. ..isAntiAlias = true;
  39. double _zero = 0;
  40. double _paddingLeft = 40;
  41. double _paddingRight = 30;
  42. double _labelHeight = 30;
  43. int _valueSize = 7;
  44. int _valueSplit = 200;
  45. int type;
  46. int decimal;
  47. List<ChartItem>? records;
  48. DateTime dateTime;
  49. late Map<String, num> values;
  50. bool drawMax = false;
  51. bool drawCurrent = false;
  52. String unit = "kal";
  53. double _max = 750;
  54. int _currentIndex = 0;
  55. double dx, dy;
  56. Chart(
  57. {this.type = 0,
  58. this.records,
  59. required this.dateTime,
  60. this.drawMax = false,
  61. this.drawCurrent = true,
  62. this.unit = "",
  63. this.decimal = 0,
  64. this.dx = 0,
  65. this.dy = 0});
  66. void initData(
  67. {double maxValue = 750, int valueSize = 5, int valueSplit = 200}) {
  68. _valueSize = valueSize;
  69. _valueSplit = valueSplit;
  70. values = {};
  71. var records = this.records ?? [];
  72. var now = DateTime.now();
  73. _currentIndex = -1;
  74. // print("$values --- $records");
  75. if (this.type == 0) {
  76. records.forEach((element) {
  77. var t = DateTime.parse(element.createdAt);
  78. values.update("${t.hour}", (value) => value + element.value,
  79. ifAbsent: () => Converter.toDouble(element.value));
  80. });
  81. for (int i = 0; i <= 24; i++) {
  82. values.putIfAbsent("$i", () => 0.0);
  83. }
  84. _currentIndex = -1;
  85. } else if (this.type == 1) {
  86. records.forEach((element) {
  87. var t = DateTime.parse(element.createdAt);
  88. values.update("${t.weekday == 0 ? 7 : t.weekday}",
  89. (value) => value + element.value,
  90. ifAbsent: () => Converter.toDouble(element.value));
  91. });
  92. for (int i = 1; i <= 7; i++) {
  93. values.putIfAbsent("$i", () => 0.0);
  94. }
  95. if (now.difference(dateTime).inDays < 7)
  96. _currentIndex = (now.weekday == 0 ? 7 : now.weekday) - 1;
  97. } else if (this.type == 2) {
  98. records.forEach((element) {
  99. var t = DateTime.parse(element.createdAt);
  100. values.update("${t.day}", (value) => value + element.value,
  101. ifAbsent: () => Converter.toDouble(element.value));
  102. });
  103. for (int i = 1; i <= 31; i++) {
  104. values.putIfAbsent("$i", () => 0.0);
  105. }
  106. if (now.difference(dateTime).inDays < 28) _currentIndex = now.day - 1;
  107. } else if (this.type == 3) {
  108. records.forEach((element) {
  109. values.update("${element.createdAt}", (value) => value + element.value,
  110. ifAbsent: () => Converter.toDouble(element.value));
  111. });
  112. for (int i = 1; i <= 12; i++) {
  113. values.putIfAbsent("$i", () => 0.0);
  114. }
  115. if (now.difference(dateTime).inDays < 360) _currentIndex = now.month - 1;
  116. } else if (this.type == 4) {
  117. records.forEach((element) {
  118. values.update("${element.createdAt}", (value) => value + element.value,
  119. ifAbsent: () => Converter.toDouble(element.value));
  120. });
  121. for (int i = 1; i <= 10; i++) {
  122. values.putIfAbsent("${now.year - i + 1}", () => 0.0);
  123. }
  124. _currentIndex = -1;
  125. }
  126. _max = 0;
  127. values.values.forEach((element) {
  128. _max = max(_max, Converter.toDouble(element));
  129. });
  130. double diff = _max % _valueSplit;
  131. double round = _max - diff;
  132. print("--- $_max $round $diff");
  133. _max = max(_valueSplit.toDouble(), _max - diff + _valueSplit);
  134. print("$values --- $_max $maxValue $diff");
  135. }
  136. @override
  137. void paint(Canvas canvas, Size size) {
  138. double zero = _zero = size.height - _labelHeight;
  139. double right = size.width - _paddingRight;
  140. // draw 值 行数
  141. int valuePadding = 10;
  142. canvas.drawLine(Offset(_paddingLeft, zero), Offset(right, zero), _paint);
  143. ParagraphBuilder pb = ParagraphBuilder(_valueStyle)
  144. ..pushStyle(ui.TextStyle(color: Color(0xff999999)))
  145. ..addText("0");
  146. ParagraphConstraints constraints =
  147. ParagraphConstraints(width: _paddingLeft - valuePadding);
  148. Paragraph paragraph = pb.build()..layout(constraints);
  149. paragraph.computeLineMetrics().forEach((element) {
  150. canvas.drawParagraph(paragraph, Offset(0, zero - element.baseline / 2));
  151. });
  152. canvas.drawLine(
  153. Offset(_paddingLeft, zero), Offset(_paddingLeft, 0), _paint);
  154. canvas.drawLine(Offset(right, zero), Offset(right, 0), _paint);
  155. double valueSpace = (size.height - _labelHeight) / _valueSize;
  156. for (var i = 0; i < _valueSize; i++) {
  157. canvas.drawLine(Offset(_paddingLeft, valueSpace * i),
  158. Offset(right, valueSpace * i), _paint);
  159. double value = (_valueSize - i) * _max / _valueSize;
  160. String label = "";
  161. if (value < 1000) {
  162. label = value == value.toInt()
  163. ? value.toInt().toString()
  164. : value.toStringAsFixed(1);
  165. } else {
  166. value /= 1000;
  167. label =
  168. "${value == value.toInt() ? value.toInt().toString() : value.toStringAsFixed(1)}k";
  169. }
  170. ParagraphBuilder pb = ParagraphBuilder(_valueStyle)
  171. ..pushStyle(ui.TextStyle(color: Color(0xff999999)))
  172. ..addText(label);
  173. ParagraphConstraints constraints =
  174. ParagraphConstraints(width: _paddingLeft - valuePadding);
  175. Paragraph paragraph = pb.build()..layout(constraints);
  176. paragraph.computeLineMetrics().forEach((element) {
  177. canvas.drawParagraph(
  178. paragraph, Offset(0, valueSpace * i - element.baseline / 2));
  179. });
  180. // print("${value / 1000} ${_max} ${(_valueSize - i) * _max ~/ _valueSize}");
  181. }
  182. // draw 数据列
  183. double width = right - _paddingRight; // 柱子的有效空间
  184. double valueStroke = 8; // 柱子宽度
  185. List<String> valueLabel = []; // 柱子数量
  186. int scaleCount = 0; // 刻度数量
  187. int scaleWeight = 3;
  188. if (this.type == 0) {
  189. // 日,按时间
  190. scaleWeight = 6;
  191. valueLabel = List.generate(4, (index) => "${index * scaleWeight}:00");
  192. // valueStroke = width / valueLabel.length / scaleWeight - 5;
  193. scaleCount = 24;
  194. } else if (this.type == 1) {
  195. width -= 40;
  196. scaleWeight = 1;
  197. valueLabel = List.generate(7, (index) => WEEK[index]);
  198. // valueStroke = 12;
  199. scaleCount = 7;
  200. } else if (this.type == 2) {
  201. DateTime now = this.dateTime;
  202. // DateTime now = DateTime(2019,2);
  203. DateTime startTime = DateTime.parse(
  204. '${now.year}-${now.month < 10 ? '0${now.month}' : now.month}-01');
  205. DateTime endTime = DateTime.parse(
  206. '${now.year}-${now.month + 1 < 10 ? '0${now.month + 1}' : now.month + 1}-01');
  207. int diffDay = endTime.difference(startTime).inDays;
  208. // print("11111111111111111111111111111 $startTime $endTime $diffDay");
  209. // List<int> days = [1, 8, 15, 22, diffDay == 28 ? 28:29];
  210. scaleCount = diffDay;
  211. scaleWeight = 5;
  212. // valueLabel = days.map((e) => '$e/${now.month}').toList();
  213. // valueLabel = List.generate(diffDay ~/ scaleWeight + (scaleCount % scaleWeight == 0 ? 1 : 0), (index) => "${index * 2 + 1}");
  214. valueLabel = List.generate(
  215. scaleCount ~/ scaleWeight + 1, (index) => '${index * scaleWeight}');
  216. valueStroke = 5;
  217. } else if (this.type == 3) {
  218. valueLabel = List.generate(12, (index) => '${index + 1}');
  219. // valueStroke = 12;
  220. scaleCount = 12;
  221. scaleWeight = 1;
  222. } else if (this.type == 4) {
  223. int len = 6;
  224. var now = DateTime.now();
  225. int start = now.year - len;
  226. valueLabel = List.generate(len, (index) => '${start + index + 1}');
  227. // valueStroke = 12;
  228. scaleCount = len;
  229. scaleWeight = 1;
  230. }
  231. int labelSize = valueLabel.length;
  232. double labelSpace = (width) / labelSize;
  233. double scaleWidth = (right - _paddingLeft) / (scaleCount + 1);
  234. // print("${size.width} $_paddingLeft $right $width $scaleWidth $scaleCount");
  235. double left = scaleWidth + _paddingLeft;
  236. if (type == 2) {
  237. left = _paddingLeft;
  238. }
  239. // if (this.type < 2) {
  240. //draw 刻度
  241. for (var i = 0; i < scaleCount; i++) {
  242. var index = scaleWidth * (i + 1);
  243. canvas.drawLine(Offset(_paddingLeft + index, zero),
  244. Offset(_paddingLeft + index, zero - 5), _paint);
  245. }
  246. // }
  247. for (var i = 0; i < labelSize; i++) {
  248. int index = i;
  249. String label = valueLabel[i];
  250. bool offset = false;
  251. if (label == "0") {
  252. label = "1";
  253. offset = true;
  254. }
  255. // if (type == 2 && i % 2 == 1) {
  256. // if (i != _currentIndex) continue;
  257. // }
  258. // print("1111111111111111111 $i $_currentIndex $label");
  259. bool curr = i == _currentIndex && drawCurrent == true;
  260. ParagraphBuilder pb = ParagraphBuilder(_labelStyle)
  261. ..pushStyle(ui.TextStyle(
  262. color: curr ? const Color(0xffFFC400) : const Color(0xff999999)))
  263. ..addText(label);
  264. ParagraphConstraints constraints =
  265. ParagraphConstraints(width: labelSpace);
  266. Paragraph paragraph = pb.build()..layout(constraints);
  267. List<LineMetrics> lines = paragraph.computeLineMetrics();
  268. for (var line = 0; line < lines.length; line++) {
  269. LineMetrics element = lines[line];
  270. var x = left + scaleWidth * scaleWeight * index - element.width / 2;
  271. if (offset) {
  272. x += scaleWidth;
  273. }
  274. var y = zero + 5;
  275. canvas.drawParagraph(paragraph, Offset(x, y));
  276. // if (curr) {
  277. // x = x + element.width / 2;
  278. // canvas.drawPath(
  279. // Path()
  280. // ..moveTo(x, y + fontSize + 5)
  281. // ..lineTo(x - 3, y + fontSize + 8)
  282. // ..lineTo(x + 3, y + fontSize + 8)
  283. // ..close(),
  284. // _paintCurrent);
  285. // }
  286. break;
  287. }
  288. if (type == 2) {}
  289. //
  290. // double l = left + scaleWidth * scaleWeight * i - valueStroke / 2;
  291. // Rect rect = Rect.fromLTRB(l, zero, l + valueStroke, 0);
  292. // print("value draw $rect");
  293. // Paint valuePaint = Paint()
  294. // ..shader = LinearGradient(
  295. // begin: Alignment.bottomCenter,
  296. // end: Alignment.topCenter,
  297. // colors: <Color>[Color(0xffFF9100), Color(0xffFFE600)],
  298. // ).createShader(rect);
  299. // canvas.drawRRect(RRect.fromRectAndRadius(rect, Radius.circular(100)), valuePaint);
  300. }
  301. num maxVal = values.values
  302. .reduce((value, element) => max(value, element).toDouble());
  303. bool _drawMax = this.drawMax;
  304. // if (this.type < 2) {
  305. for (var layer = 0; layer < 3; layer++) {
  306. for (var i = 0; i < scaleCount; i++) {
  307. String key = type == 4
  308. ? valueLabel[i]
  309. : (type == 0)
  310. ? '$i'
  311. : '${i + 1}';
  312. if (!values.containsKey(key)) continue;
  313. num value = values[key]!;
  314. double l = left + scaleWidth * i - valueStroke / 2 + ((type==2)?scaleWidth:0);
  315. // print("$key ${values[key]} 11111111111111 $scaleCount");
  316. Rect rect = Rect.fromLTRB(l, calValue(value), l + valueStroke, zero);
  317. if(layer == 0) {
  318. Paint valuePaint = Paint()
  319. ..shader = LinearGradient(
  320. begin: Alignment.topCenter,
  321. end: Alignment.bottomCenter,
  322. colors: <Color>[Color(0xffFF9100), Color(0xffFFE600)],
  323. ).createShader(rect);
  324. canvas.drawRRect(
  325. RRect.fromRectAndRadius(rect, Radius.circular(100)), valuePaint);
  326. }
  327. // canvas.drawRRect(RRect.fromRectAndRadius(rect, Radius.circular(100)), columnPaint);
  328. String _label =
  329. "${this.decimal == 0 ? value.round() : value.toStringAsFixed(this.decimal)}${this.unit}";
  330. if (layer == 1) {
  331. double _dx = dx;
  332. if (rect.contains((ui.Offset(_dx, dy)))) {
  333. _drawMax = false;
  334. double width = 140;
  335. ParagraphBuilder unit = ParagraphBuilder(_labelStyle)
  336. ..pushStyle(ui.TextStyle(color: Colors.white))
  337. ..addText("$_label");
  338. ParagraphConstraints pc = ParagraphConstraints(width: width);
  339. paragraph = unit.build()..layout(pc);
  340. paragraph.computeLineMetrics().forEach((element) {
  341. Rect tips = Rect.fromCenter(
  342. center: Offset(rect.center.dx, rect.top - element.height * 1.5),
  343. width: element.width + 12,
  344. height: element.height * 2);
  345. Paint bgPaint = Paint()..color = Colors.black87;
  346. Path path = Path();
  347. path.addRRect(RRect.fromRectAndRadius(tips, Radius.circular(6)));
  348. path.moveTo(rect.center.dx, tips.bottom + 5);
  349. path.lineTo(rect.center.dx - 5, tips.bottom);
  350. path.lineTo(rect.center.dx + 5, tips.bottom);
  351. canvas.drawPath(path, bgPaint);
  352. var x = l + valueStroke / 2 - element.width / 2;
  353. canvas.drawParagraph(paragraph,
  354. Offset(x, tips.center.dy - element.height / 2 + 1));
  355. });
  356. }
  357. }
  358. if (layer == 2 && _drawMax) {
  359. if (maxVal <= value) {
  360. if (maxVal > 0) {
  361. double offsetY = calValue(maxVal);
  362. double offsetX = _paddingLeft;
  363. while (offsetX < right) {
  364. canvas.drawLine(Offset(offsetX, offsetY),
  365. Offset(min(offsetX + 3, right), offsetY), _maxPaint);
  366. offsetX += 5;
  367. }
  368. ParagraphBuilder unit = ParagraphBuilder(_labelStyle)
  369. ..pushStyle(ui.TextStyle(color: _maxColor))
  370. ..addText("$_label");
  371. ParagraphConstraints pc = ParagraphConstraints(width: size.width);
  372. paragraph = unit.build()..layout(pc);
  373. paragraph.computeLineMetrics().forEach((element) {
  374. var x = l + valueStroke / 2 - element.width / 2;
  375. canvas.drawParagraph(
  376. paragraph, Offset(x, offsetY - element.baseline - 10));
  377. x = l + valueStroke / 2;
  378. canvas.drawPath(
  379. Path()
  380. ..moveTo(x, offsetY - 4)
  381. ..lineTo(x - 3, offsetY - 7)
  382. ..lineTo(x + 3, offsetY - 7)
  383. ..close(),
  384. _maxPaint);
  385. });
  386. }
  387. }
  388. }
  389. }
  390. }
  391. // } else {
  392. // Path path = Path();
  393. // path.moveTo(_paddingLeft, calValue(values['1']));
  394. // Path area = Path()
  395. // ..moveTo(_paddingLeft, zero)
  396. // ..lineTo(_paddingLeft, calValue(values['1']));
  397. // for (var i = 1; i <= scaleCount; i++) {
  398. // path.lineTo(left + scaleWidth * i, calValue(values['$i']));
  399. // area.lineTo(left + scaleWidth * i, calValue(values['$i']));
  400. // }
  401. // area.lineTo(size.width, zero);
  402. // area.close();
  403. //
  404. // canvas.save();
  405. // canvas.clipPath(area);
  406. // Rect rect = Rect.fromLTRB(_paddingLeft, zero, size.width, calValue(_max));
  407. // Paint valuePaint = Paint()
  408. // ..shader = LinearGradient(
  409. // begin: Alignment.bottomCenter,
  410. // end: Alignment.topCenter,
  411. // colors: <Color>[Color(0xffFFC400), Color(0x50FFFFFF)],
  412. // ).createShader(rect);
  413. // canvas.drawRect(rect, valuePaint);
  414. // canvas.restore();
  415. //
  416. // canvas.drawPath(path, _linePaint);
  417. // }
  418. }
  419. double calValue(num value) {
  420. if (value == null) return _zero;
  421. return max(0, _zero - _zero * value / _max);
  422. }
  423. @override
  424. bool shouldRepaint(CustomPainter oldDelegate) {
  425. return false;
  426. }
  427. }