chart_painter.dart 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. import 'dart:async' show StreamSink;
  2. import 'package:flutter/material.dart';
  3. import 'package:k_chart_plus/utils/number_util.dart';
  4. import '../entity/info_window_entity.dart';
  5. import '../entity/k_line_entity.dart';
  6. import '../utils/date_format_util.dart';
  7. import 'base_chart_painter.dart';
  8. import 'base_chart_renderer.dart';
  9. import 'base_dimension.dart';
  10. import 'main_renderer.dart';
  11. import 'secondary_renderer.dart';
  12. import 'vol_renderer.dart';
  13. class TrendLine {
  14. final Offset p1;
  15. final Offset p2;
  16. final double maxHeight;
  17. final double scale;
  18. TrendLine(this.p1, this.p2, this.maxHeight, this.scale);
  19. }
  20. double? trendLineX;
  21. double getTrendLineX() {
  22. return trendLineX ?? 0;
  23. }
  24. class ChartPainter extends BaseChartPainter {
  25. final List<TrendLine> lines; //For TrendLine
  26. final bool isTrendLine; //For TrendLine
  27. bool isrecordingCord = false; //For TrendLine
  28. final double selectY; //For TrendLine
  29. static get maxScrollX => BaseChartPainter.maxScrollX;
  30. late BaseChartRenderer mMainRenderer;
  31. BaseChartRenderer? mVolRenderer;
  32. Set<BaseChartRenderer> mSecondaryRendererList = {};
  33. StreamSink<InfoWindowEntity?> sink;
  34. Color? upColor, dnColor;
  35. Color? ma5Color, ma10Color, ma30Color;
  36. Color? volColor;
  37. Color? macdColor, difColor, deaColor, jColor;
  38. int fixedLength;
  39. List<int> maDayList;
  40. final ChartColors chartColors;
  41. late Paint selectPointPaint, selectorBorderPaint, nowPricePaint;
  42. final ChartStyle chartStyle;
  43. final bool hideGrid;
  44. final bool showNowPrice;
  45. final VerticalTextAlignment verticalTextAlignment;
  46. final BaseDimension baseDimension;
  47. /// 外部传入的当前价格字符串(来自 WS ticker),优先于 toStringAsFixed 显示
  48. final String? nowPriceStr;
  49. ChartPainter(
  50. this.chartStyle,
  51. this.chartColors, {
  52. required this.lines, //For TrendLine
  53. required this.isTrendLine, //For TrendLine
  54. required this.selectY, //For TrendLine
  55. required this.sink,
  56. required datas,
  57. required scaleX,
  58. required scrollX,
  59. required isLongPass,
  60. required selectX,
  61. required xFrontPadding,
  62. required this.baseDimension,
  63. isOnTap,
  64. isTapShowInfoDialog,
  65. required this.verticalTextAlignment,
  66. mainStateLi,
  67. volHidden,
  68. secondaryStateLi,
  69. bool isLine = false,
  70. this.hideGrid = false,
  71. this.showNowPrice = true,
  72. this.fixedLength = 2,
  73. this.maDayList = const [5, 10, 20],
  74. this.nowPriceStr,
  75. }) : super(chartStyle,
  76. datas: datas,
  77. scaleX: scaleX,
  78. scrollX: scrollX,
  79. isLongPress: isLongPass,
  80. baseDimension: baseDimension,
  81. isOnTap: isOnTap,
  82. isTapShowInfoDialog: isTapShowInfoDialog,
  83. selectX: selectX,
  84. mainStateLi: mainStateLi,
  85. volHidden: volHidden,
  86. secondaryStateLi: secondaryStateLi,
  87. xFrontPadding: xFrontPadding,
  88. isLine: isLine) {
  89. selectPointPaint = Paint()
  90. ..isAntiAlias = true
  91. ..strokeWidth = 0.5
  92. ..color = this.chartColors.selectFillColor;
  93. selectorBorderPaint = Paint()
  94. ..isAntiAlias = true
  95. ..strokeWidth = 0.5
  96. ..style = PaintingStyle.stroke
  97. ..color = this.chartColors.selectBorderColor;
  98. nowPricePaint = Paint()
  99. ..strokeWidth = this.chartStyle.nowPriceLineWidth
  100. ..isAntiAlias = true;
  101. }
  102. @override
  103. void initChartRenderer() {
  104. if (datas != null && datas!.isNotEmpty) {
  105. var t = datas![0];
  106. fixedLength =
  107. NumberUtil.getMaxDecimalLength(t.open, t.close, t.high, t.low);
  108. }
  109. mMainRenderer = MainRenderer(
  110. mMainRect,
  111. mMainMaxValue,
  112. mMainMinValue,
  113. mTopPadding,
  114. mainStateLi.toList(),
  115. isLine,
  116. fixedLength,
  117. this.chartStyle,
  118. this.chartColors,
  119. this.scaleX,
  120. verticalTextAlignment,
  121. maDayList,
  122. );
  123. if (mVolRect != null) {
  124. mVolRenderer = VolRenderer(mVolRect!, mVolMaxValue, mVolMinValue,
  125. mChildPadding, fixedLength, this.chartStyle, this.chartColors);
  126. }
  127. mSecondaryRendererList.clear();
  128. for (int i = 0; i < mSecondaryRectList.length; ++i) {
  129. mSecondaryRendererList.add(SecondaryRenderer(
  130. mSecondaryRectList[i].mRect,
  131. mSecondaryRectList[i].mMaxValue,
  132. mSecondaryRectList[i].mMinValue,
  133. mChildPadding,
  134. secondaryStateLi.elementAt(i),
  135. fixedLength,
  136. chartStyle,
  137. chartColors,
  138. ));
  139. }
  140. }
  141. @override
  142. void drawBg(Canvas canvas, Size size) {
  143. Paint mBgPaint = Paint()..color = chartColors.bgColor;
  144. Rect mainRect =
  145. Rect.fromLTRB(0, 0, mMainRect.width, mMainRect.height + mTopPadding);
  146. canvas.drawRect(mainRect, mBgPaint);
  147. if (mVolRect != null) {
  148. Rect volRect = Rect.fromLTRB(
  149. 0, mVolRect!.top - mChildPadding, mVolRect!.width, mVolRect!.bottom);
  150. canvas.drawRect(volRect, mBgPaint);
  151. }
  152. for (int i = 0; i < mSecondaryRectList.length; ++i) {
  153. Rect? mSecondaryRect = mSecondaryRectList[i].mRect;
  154. Rect secondaryRect = Rect.fromLTRB(0, mSecondaryRect.top - mChildPadding,
  155. mSecondaryRect.width, mSecondaryRect.bottom);
  156. canvas.drawRect(secondaryRect, mBgPaint);
  157. }
  158. Rect dateRect =
  159. Rect.fromLTRB(0, size.height - mBottomPadding, size.width, size.height);
  160. canvas.drawRect(dateRect, mBgPaint);
  161. }
  162. @override
  163. void drawGrid(canvas) {
  164. if (!hideGrid) {
  165. mMainRenderer.drawGrid(canvas, mGridRows, mGridColumns);
  166. mVolRenderer?.drawGrid(canvas, mGridRows, mGridColumns);
  167. mSecondaryRendererList.forEach((element) {
  168. element.drawGrid(canvas, mGridRows, mGridColumns);
  169. });
  170. }
  171. }
  172. @override
  173. void drawChart(Canvas canvas, Size size) {
  174. canvas.save();
  175. canvas.translate(mTranslateX * scaleX, 0.0);
  176. canvas.scale(scaleX, 1.0);
  177. for (int i = mStartIndex; datas != null && i <= mStopIndex; i++) {
  178. KLineEntity? curPoint = datas?[i];
  179. if (curPoint == null) continue;
  180. KLineEntity lastPoint = i == 0 ? curPoint : datas![i - 1];
  181. double curX = getX(i);
  182. double lastX = i == 0 ? curX : getX(i - 1);
  183. mMainRenderer.drawChart(lastPoint, curPoint, lastX, curX, size, canvas);
  184. mVolRenderer?.drawChart(lastPoint, curPoint, lastX, curX, size, canvas);
  185. mSecondaryRendererList.forEach((element) {
  186. element.drawChart(lastPoint, curPoint, lastX, curX, size, canvas);
  187. });
  188. }
  189. if ((isLongPress == true || (isTapShowInfoDialog && isOnTap)) &&
  190. isTrendLine == false) {
  191. drawCrossLine(canvas, size);
  192. }
  193. if (isTrendLine == true) drawTrendLines(canvas, size);
  194. canvas.restore();
  195. }
  196. @override
  197. void drawVerticalText(canvas) {
  198. var textStyle = getTextStyle(this.chartColors.defaultTextColor);
  199. if (!hideGrid) {
  200. mMainRenderer.drawVerticalText(canvas, textStyle, mGridRows);
  201. }
  202. mVolRenderer?.drawVerticalText(canvas, textStyle, mGridRows);
  203. mSecondaryRendererList.forEach((element) {
  204. element.drawVerticalText(canvas, textStyle, mGridRows);
  205. });
  206. }
  207. @override
  208. void drawDate(Canvas canvas, Size size) {
  209. if (datas == null) return;
  210. double columnSpace = size.width / mGridColumns;
  211. double startX = getX(mStartIndex) - mPointWidth / 2;
  212. double stopX = getX(mStopIndex) + mPointWidth / 2;
  213. double x = 0.0;
  214. double y = 0.0;
  215. for (var i = 0; i <= mGridColumns; ++i) {
  216. double translateX = xToTranslateX(columnSpace * i);
  217. if (translateX >= startX && translateX <= stopX) {
  218. int index = indexOfTranslateX(translateX);
  219. if (datas?[index] == null) continue;
  220. TextPainter tp = getTextPainter(getDate(datas![index].time), null);
  221. // 时间轴绘制在图表底部 bottomPadding 区域,避免与 VOL 标签重叠
  222. y = size.height - mBottomPadding + (mBottomPadding - tp.height) / 2;
  223. x = columnSpace * i - tp.width / 2;
  224. // Prevent date text out of canvas
  225. if (x < 0) x = 0;
  226. if (x > size.width - tp.width) x = size.width - tp.width;
  227. tp.paint(canvas, Offset(x, y));
  228. }
  229. }
  230. // double translateX = xToTranslateX(0);
  231. // if (translateX >= startX && translateX <= stopX) {
  232. // TextPainter tp = getTextPainter(getDate(datas[mStartIndex].id));
  233. // tp.paint(canvas, Offset(0, y));
  234. // }
  235. // translateX = xToTranslateX(size.width);
  236. // if (translateX >= startX && translateX <= stopX) {
  237. // TextPainter tp = getTextPainter(getDate(datas[mStopIndex].id));
  238. // tp.paint(canvas, Offset(size.width - tp.width, y));
  239. // }
  240. }
  241. /// draw the cross line. when user focus
  242. @override
  243. void drawCrossLineText(Canvas canvas, Size size) {
  244. var index = calculateSelectedX(selectX);
  245. KLineEntity point = getItem(index);
  246. TextPainter tp = getTextPainter(point.close, chartColors.crossTextColor);
  247. double textHeight = tp.height;
  248. double textWidth = tp.width;
  249. double w1 = 5;
  250. double w2 = 3;
  251. double r = textHeight / 2 + w2;
  252. double y = getMainY(point.close);
  253. double x;
  254. bool isLeft = false;
  255. if (translateXtoX(getX(index)) < mWidth / 2) {
  256. isLeft = false;
  257. x = 1;
  258. Path path = new Path();
  259. path.moveTo(x, y - r);
  260. path.lineTo(x, y + r);
  261. path.lineTo(textWidth + 2 * w1, y + r);
  262. path.lineTo(textWidth + 2 * w1 + w2, y);
  263. path.lineTo(textWidth + 2 * w1, y - r);
  264. path.close();
  265. canvas.drawPath(path, selectPointPaint);
  266. canvas.drawPath(path, selectorBorderPaint);
  267. tp.paint(canvas, Offset(x + w1, y - textHeight / 2));
  268. } else {
  269. isLeft = true;
  270. x = mWidth - textWidth - 1 - 2 * w1 - w2;
  271. Path path = new Path();
  272. path.moveTo(x, y);
  273. path.lineTo(x + w2, y + r);
  274. path.lineTo(mWidth - 2, y + r);
  275. path.lineTo(mWidth - 2, y - r);
  276. path.lineTo(x + w2, y - r);
  277. path.close();
  278. canvas.drawPath(path, selectPointPaint);
  279. canvas.drawPath(path, selectorBorderPaint);
  280. tp.paint(canvas, Offset(x + w1 + w2, y - textHeight / 2));
  281. }
  282. TextPainter dateTp =
  283. getTextPainter(getDate(point.time), chartColors.crossTextColor);
  284. textWidth = dateTp.width;
  285. r = textHeight / 2;
  286. x = translateXtoX(getX(index));
  287. y = size.height - mBottomPadding;
  288. if (x < textWidth + 2 * w1) {
  289. x = 1 + textWidth / 2 + w1;
  290. } else if (mWidth - x < textWidth + 2 * w1) {
  291. x = mWidth - 1 - textWidth / 2 - w1;
  292. }
  293. double baseLine = textHeight / 2;
  294. canvas.drawRect(
  295. Rect.fromLTRB(x - textWidth / 2 - w1, y, x + textWidth / 2 + w1,
  296. y + baseLine + r),
  297. selectPointPaint);
  298. canvas.drawRect(
  299. Rect.fromLTRB(x - textWidth / 2 - w1, y, x + textWidth / 2 + w1,
  300. y + baseLine + r),
  301. selectorBorderPaint);
  302. dateTp.paint(canvas, Offset(x - textWidth / 2, y));
  303. //Long press to display the details of this data
  304. sink.add(InfoWindowEntity(point, isLeft: isLeft));
  305. }
  306. @override
  307. void drawText(Canvas canvas, KLineEntity data, double x) {
  308. //Long press to display the data in the press
  309. if (isLongPress || (isTapShowInfoDialog && isOnTap)) {
  310. var index = calculateSelectedX(selectX);
  311. data = getItem(index);
  312. }
  313. //Release to display the last data
  314. mMainRenderer.drawText(canvas, data, x);
  315. mVolRenderer?.drawText(canvas, data, x);
  316. mSecondaryRendererList.forEach((element) {
  317. element.drawText(canvas, data, x);
  318. });
  319. }
  320. @override
  321. void drawMaxAndMin(Canvas canvas) {
  322. if (isLine == true) return;
  323. //plot maxima and minima
  324. double x = translateXtoX(getX(mMainMinIndex));
  325. double y = getMainY(mMainLowMinValue);
  326. if (x < mWidth / 2) {
  327. //draw right
  328. TextPainter tp = getTextPainter(
  329. "── " + mMainLowMinValue.toStringAsFixed(fixedLength),
  330. chartColors.minColor);
  331. tp.paint(canvas, Offset(x, y - tp.height / 2));
  332. } else {
  333. TextPainter tp = getTextPainter(
  334. mMainLowMinValue.toStringAsFixed(fixedLength) + " ──",
  335. chartColors.minColor);
  336. tp.paint(canvas, Offset(x - tp.width, y - tp.height / 2));
  337. }
  338. x = translateXtoX(getX(mMainMaxIndex));
  339. y = getMainY(mMainHighMaxValue);
  340. if (x < mWidth / 2) {
  341. //draw right
  342. TextPainter tp = getTextPainter(
  343. "── " + mMainHighMaxValue.toStringAsFixed(fixedLength),
  344. chartColors.maxColor);
  345. tp.paint(canvas, Offset(x, y - tp.height / 2));
  346. } else {
  347. TextPainter tp = getTextPainter(
  348. mMainHighMaxValue.toStringAsFixed(fixedLength) + " ──",
  349. chartColors.maxColor);
  350. tp.paint(canvas, Offset(x - tp.width, y - tp.height / 2));
  351. }
  352. }
  353. @override
  354. void drawNowPrice(Canvas canvas) {
  355. if (!this.showNowPrice) {
  356. return;
  357. }
  358. if (datas == null) {
  359. return;
  360. }
  361. double value = datas!.last.close;
  362. double y = getMainY(value);
  363. //view display area boundary value drawing
  364. if (y > getMainY(mMainLowMinValue)) {
  365. y = getMainY(mMainLowMinValue);
  366. }
  367. if (y < getMainY(mMainHighMaxValue)) {
  368. y = getMainY(mMainHighMaxValue);
  369. }
  370. nowPricePaint
  371. ..color = value >= datas!.last.open
  372. ? this.chartColors.nowPriceUpColor
  373. : this.chartColors.nowPriceDnColor;
  374. //first draw the horizontal line
  375. double startX = 0;
  376. final max = -mTranslateX + mWidth / scaleX;
  377. final space =
  378. this.chartStyle.nowPriceLineSpan + this.chartStyle.nowPriceLineLength;
  379. while (startX < max) {
  380. canvas.drawLine(
  381. Offset(startX, y),
  382. Offset(startX + this.chartStyle.nowPriceLineLength, y),
  383. nowPricePaint);
  384. startX += space;
  385. }
  386. //repaint the background and text
  387. TextPainter tp = getTextPainter(
  388. nowPriceStr ?? value.toStringAsFixed(fixedLength),
  389. this.chartColors.nowPriceTextColor,
  390. );
  391. double offsetX;
  392. switch (verticalTextAlignment) {
  393. case VerticalTextAlignment.left:
  394. offsetX = 0;
  395. break;
  396. case VerticalTextAlignment.right:
  397. offsetX = mWidth - tp.width;
  398. break;
  399. }
  400. double top = y - tp.height / 2;
  401. canvas.drawRect(
  402. Rect.fromLTRB(offsetX, top, offsetX + tp.width, top + tp.height),
  403. nowPricePaint);
  404. tp.paint(canvas, Offset(offsetX, top));
  405. }
  406. //For TrendLine
  407. void drawTrendLines(Canvas canvas, Size size) {
  408. var index = calculateSelectedX(selectX);
  409. Paint paintY = Paint()
  410. ..color = chartColors.trendLineColor
  411. ..strokeWidth = 1
  412. ..isAntiAlias = true;
  413. double x = getX(index);
  414. trendLineX = x;
  415. double y = selectY;
  416. // getMainY(point.close);
  417. // K-line chart vertical line
  418. canvas.drawLine(Offset(x, mTopPadding),
  419. Offset(x, size.height - mBottomPadding), paintY);
  420. Paint paintX = Paint()
  421. ..color = chartColors.trendLineColor
  422. ..strokeWidth = 1
  423. ..isAntiAlias = true;
  424. Paint paint = Paint()
  425. ..color = chartColors.trendLineColor
  426. ..strokeWidth = 1.0
  427. ..style = PaintingStyle.stroke
  428. ..strokeCap = StrokeCap.round;
  429. canvas.drawLine(Offset(-mTranslateX, y),
  430. Offset(-mTranslateX + mWidth / scaleX, y), paintX);
  431. if (scaleX >= 1) {
  432. canvas.drawOval(
  433. Rect.fromCenter(
  434. center: Offset(x, y), height: 15.0 * scaleX, width: 15.0),
  435. paint,
  436. );
  437. } else {
  438. canvas.drawOval(
  439. Rect.fromCenter(
  440. center: Offset(x, y), height: 10.0, width: 10.0 / scaleX),
  441. paint,
  442. );
  443. }
  444. if (lines.isNotEmpty) {
  445. lines.forEach((element) {
  446. var y1 = -((element.p1.dy - 35) / element.scale) + element.maxHeight;
  447. var y2 = -((element.p2.dy - 35) / element.scale) + element.maxHeight;
  448. var a = (trendLineMax! - y1) * trendLineScale! + trendLineContentRec!;
  449. var b = (trendLineMax! - y2) * trendLineScale! + trendLineContentRec!;
  450. var p1 = Offset(element.p1.dx, a);
  451. var p2 = Offset(element.p2.dx, b);
  452. canvas.drawLine(
  453. p1,
  454. element.p2 == Offset(-1, -1) ? Offset(x, y) : p2,
  455. Paint()
  456. ..color = Colors.yellow
  457. ..strokeWidth = 2);
  458. });
  459. }
  460. }
  461. ///draw cross lines
  462. void drawCrossLine(Canvas canvas, Size size) {
  463. var index = calculateSelectedX(selectX);
  464. KLineEntity point = getItem(index);
  465. Paint paintY = Paint()
  466. ..color = this.chartColors.vCrossColor
  467. ..strokeWidth = this.chartStyle.vCrossWidth
  468. ..isAntiAlias = true;
  469. double x = getX(index);
  470. double y = getMainY(point.close);
  471. // K-line chart vertical line
  472. canvas.drawLine(Offset(x, mTopPadding),
  473. Offset(x, size.height - mBottomPadding), paintY);
  474. Paint paintX = Paint()
  475. ..color = this.chartColors.hCrossColor
  476. ..strokeWidth = this.chartStyle.hCrossWidth
  477. ..isAntiAlias = true;
  478. // K-line chart horizontal line
  479. canvas.drawLine(Offset(-mTranslateX, y),
  480. Offset(-mTranslateX + mWidth / scaleX, y), paintX);
  481. if (scaleX >= 1) {
  482. canvas.drawOval(
  483. Rect.fromCenter(center: Offset(x, y), height: 2.0 * scaleX, width: 2.0),
  484. paintX,
  485. );
  486. } else {
  487. canvas.drawOval(
  488. Rect.fromCenter(center: Offset(x, y), height: 2.0, width: 2.0 / scaleX),
  489. paintX,
  490. );
  491. }
  492. }
  493. TextPainter getTextPainter(text, color) {
  494. if (color == null) {
  495. color = this.chartColors.defaultTextColor;
  496. }
  497. TextSpan span = TextSpan(text: "$text", style: getTextStyle(color));
  498. TextPainter tp = TextPainter(text: span, textDirection: TextDirection.ltr);
  499. tp.layout();
  500. return tp;
  501. }
  502. String getDate(int? date) => dateFormat(
  503. DateTime.fromMillisecondsSinceEpoch(
  504. date ?? DateTime.now().millisecondsSinceEpoch),
  505. mFormats,
  506. );
  507. double getMainY(double y) => mMainRenderer.getY(y);
  508. /// Whether the point is in the SecondaryRect
  509. // bool isInSecondaryRect(Offset point) {
  510. // // return mSecondaryRect.contains(point) == true);
  511. // return false;
  512. // }
  513. /// Whether the point is in MainRect
  514. bool isInMainRect(Offset point) {
  515. return mMainRect.contains(point);
  516. }
  517. }