| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434 |
- import 'dart:math';
- import 'package:flutter/material.dart'
- show Color, TextStyle, Rect, Canvas, Size, CustomPainter;
- import 'package:k_chart_plus/utils/date_format_util.dart';
- import '../chart_style.dart' show ChartStyle;
- import '../entity/k_line_entity.dart';
- import '../k_chart_widget.dart';
- import 'base_dimension.dart';
- export 'package:flutter/material.dart'
- show Color, required, TextStyle, Rect, Canvas, Size, CustomPainter;
- /// BaseChartPainter
- abstract class BaseChartPainter extends CustomPainter {
- static double maxScrollX = 0.0;
- List<KLineEntity>? datas; // data of chart
- Set<MainState> mainStateLi; //MainState mainState;
- Set<SecondaryState> secondaryStateLi;
- bool volHidden;
- bool isTapShowInfoDialog;
- double scaleX = 1.0, scrollX = 0.0, selectX;
- bool isLongPress = false;
- bool isOnTap;
- bool isLine;
- late Rect mMainLabelRect;
- /// Rectangle box of main chart
- late Rect mMainRect;
- /// Rectangle box of the vol chart
- Rect? mVolRect;
- /// Secondary list support
- List<RenderRect> mSecondaryRectList = [];
- late double mDisplayHeight, mWidth;
- // padding
- double mTopPadding = 20.0, mBottomPadding = 20.0, mChildPadding = 12.0;
- // grid: rows - columns
- int mGridRows = 4, mGridColumns = 4;
- int mStartIndex = 0, mStopIndex = 0;
- double mMainMaxValue = double.minPositive, mMainMinValue = double.maxFinite;
- double mVolMaxValue = double.minPositive, mVolMinValue = double.maxFinite;
- double mTranslateX = double.minPositive;
- int mMainMaxIndex = 0, mMainMinIndex = 0;
- double mMainHighMaxValue = double.minPositive,
- mMainLowMinValue = double.maxFinite;
- int mItemCount = 0;
- double mDataLen = 0.0; // the data occupies the total length of the screen
- final ChartStyle chartStyle;
- late double mPointWidth;
- // format time
- List<String> mFormats = [yyyy, '-', mm, '-', dd, ' ', HH, ':', nn];
- double xFrontPadding;
- /// base dimension
- final BaseDimension baseDimension;
- /// constructor BaseChartPainter
- ///
- BaseChartPainter(
- this.chartStyle, {
- this.datas,
- required this.scaleX,
- required this.scrollX,
- required this.isLongPress,
- required this.selectX,
- required this.xFrontPadding,
- required this.baseDimension,
- this.isOnTap = false,
- this.mainStateLi = const <MainState>{},
- this.volHidden = false,
- this.isTapShowInfoDialog = false,
- this.secondaryStateLi = const <SecondaryState>{},
- this.isLine = false,
- }) {
- mItemCount = datas?.length ?? 0;
- mPointWidth = this.chartStyle.pointWidth;
- mTopPadding = this.chartStyle.topPadding +
- baseDimension.totalLabelHeight; // space to display text of main chart
- mBottomPadding = this.chartStyle.bottomPadding;
- mChildPadding = this.chartStyle.childPadding;
- mGridRows = this.chartStyle.gridRows;
- mGridColumns = this.chartStyle.gridColumns;
- mDataLen = mItemCount * mPointWidth;
- initFormats();
- }
- /// init format time
- void initFormats() {
- if (this.chartStyle.dateTimeFormat != null) {
- mFormats = this.chartStyle.dateTimeFormat!;
- return;
- }
- if (mItemCount < 2) {
- mFormats = [yyyy, '-', mm, '-', dd, ' ', HH, ':', nn];
- return;
- }
- int firstTime = datas!.first.time ?? 0;
- int secondTime = datas![1].time ?? 0;
- int time = secondTime - firstTime;
- time ~/= 1000;
- // monthly line
- if (time >= 24 * 60 * 60 * 28) {
- mFormats = [yy, '-', mm];
- } else if (time >= 24 * 60 * 60) {
- // daily line
- mFormats = [yy, '-', mm, '-', dd];
- } else {
- // hour line
- mFormats = [mm, '-', dd, ' ', HH, ':', nn];
- }
- }
- /// paint chart
- @override
- void paint(Canvas canvas, Size size) {
- canvas.clipRect(Rect.fromLTRB(0, 0, size.width, size.height));
- mDisplayHeight = size.height - mTopPadding - mBottomPadding;
- mWidth = size.width;
- initRect(size);
- calculateValue();
- initChartRenderer();
- canvas.save();
- canvas.scale(1, 1);
- drawBg(canvas, size);
- drawGrid(canvas);
- if (datas != null && datas!.isNotEmpty) {
- drawChart(canvas, size);
- drawVerticalText(canvas);
- drawDate(canvas, size);
- drawText(canvas, datas!.last, 5);
- drawMaxAndMin(canvas);
- drawNowPrice(canvas);
- if (isLongPress == true || (isTapShowInfoDialog && isOnTap)) {
- drawCrossLineText(canvas, size);
- }
- }
- canvas.restore();
- }
- /// init chart renderer
- void initChartRenderer();
- /// draw the background of chart
- void drawBg(Canvas canvas, Size size);
- /// draw the grid of chart
- void drawGrid(canvas);
- /// draw chart
- void drawChart(Canvas canvas, Size size);
- /// draw vertical text
- void drawVerticalText(canvas);
- /// draw date
- void drawDate(Canvas canvas, Size size);
- /// draw text
- void drawText(Canvas canvas, KLineEntity data, double x);
- /// draw maximum and minimum values
- void drawMaxAndMin(Canvas canvas);
- /// draw the current price
- void drawNowPrice(Canvas canvas);
- /// draw cross line
- void drawCrossLine(Canvas canvas, Size size);
- /// draw text of the cross line
- void drawCrossLineText(Canvas canvas, Size size);
- /// init the rectangle box to draw chart
- void initRect(Size size) {
- double volHeight = baseDimension.mVolumeHeight;
- double secondaryHeight = baseDimension.mSecondaryHeight;
- double mainHeight = mDisplayHeight;
- mainHeight -= volHeight;
- mainHeight -= baseDimension.totalSecondaryHeight;
- mMainRect = Rect.fromLTRB(0, mTopPadding, mWidth, mTopPadding + mainHeight);
- if (volHidden != true) {
- mVolRect = Rect.fromLTRB(0, mMainRect.bottom + mChildPadding, mWidth,
- mMainRect.bottom + volHeight);
- }
- mSecondaryRectList.clear();
- for (int i = 0; i < secondaryStateLi.length; ++i) {
- mSecondaryRectList.add(RenderRect(
- Rect.fromLTRB(
- 0,
- mMainRect.bottom + volHeight + i * secondaryHeight + mChildPadding,
- mWidth,
- mMainRect.bottom +
- volHeight +
- i * secondaryHeight +
- secondaryHeight),
- ));
- }
- }
- /// calculate values
- calculateValue() {
- if (datas == null) return;
- if (datas!.isEmpty) return;
- maxScrollX = getMinTranslateX().abs();
- setTranslateXFromScrollX(scrollX);
- mStartIndex = indexOfTranslateX(xToTranslateX(0));
- mStopIndex = indexOfTranslateX(xToTranslateX(mWidth));
- for (int i = mStartIndex; i <= mStopIndex; i++) {
- var item = datas![i];
- getMainMaxMinValue(item, i);
- getVolMaxMinValue(item);
- for (int idx = 0; idx < mSecondaryRectList.length; ++idx) {
- getSecondaryMaxMinValue(idx, item);
- }
- }
- }
- /// compute maximum and minimum value
- void getMainMaxMinValue(KLineEntity item, int i) {
- double maxPrice = item.high;
- double minPrice = item.low;
- for (int i = 0; i < mainStateLi.length; ++i) {
- if (mainStateLi.elementAt(i) == MainState.MA) {
- maxPrice = max(maxPrice, _findMaxMA(item.maValueList ?? [0]));
- minPrice = min(minPrice, _findMinMA(item.maValueList ?? [0]));
- } else if (mainStateLi.elementAt(i) == MainState.BOLL) {
- maxPrice = max(maxPrice, item.up ?? 0);
- minPrice = min(minPrice, item.dn ?? 0);
- } else if (mainStateLi.elementAt(i) == MainState.SAR) {
- maxPrice = max(maxPrice, item.sar ?? 0);
- minPrice = min(minPrice, item.sar ?? 0);
- }
- }
- mMainMaxValue = max(mMainMaxValue, maxPrice);
- mMainMinValue = min(mMainMinValue, minPrice);
- if (mMainHighMaxValue < item.high) {
- mMainHighMaxValue = item.high;
- mMainMaxIndex = i;
- }
- if (mMainLowMinValue > item.low) {
- mMainLowMinValue = item.low;
- mMainMinIndex = i;
- }
- if (isLine == true) {
- mMainMaxValue = max(mMainMaxValue, item.close);
- mMainMinValue = min(mMainMinValue, item.close);
- }
- }
- // find maximum of the MA
- double _findMaxMA(List<double> a) {
- double result = double.minPositive;
- for (double i in a) {
- result = max(result, i);
- }
- return result;
- }
- // find minimum of the MA
- double _findMinMA(List<double> a) {
- double result = double.maxFinite;
- for (double i in a) {
- result = min(result, i == 0 ? double.maxFinite : i);
- }
- return result;
- }
- // get the maximum and minimum of the Vol value
- void getVolMaxMinValue(KLineEntity item) {
- mVolMaxValue = max(mVolMaxValue,
- max(item.vol, max(item.MA5Volume ?? 0, item.MA10Volume ?? 0)));
- mVolMinValue = min(mVolMinValue,
- min(item.vol, min(item.MA5Volume ?? 0, item.MA10Volume ?? 0)));
- }
- // compute maximum and minimum of secondary value
- getSecondaryMaxMinValue(int index, KLineEntity item) {
- SecondaryState secondaryState = secondaryStateLi.elementAt(index);
- switch (secondaryState) {
- // MACD
- case SecondaryState.MACD:
- if (item.macd != null) {
- mSecondaryRectList[index].mMaxValue = max(
- mSecondaryRectList[index].mMaxValue,
- max(item.macd!, max(item.dif!, item.dea!)));
- mSecondaryRectList[index].mMinValue = min(
- mSecondaryRectList[index].mMinValue,
- min(item.macd!, min(item.dif!, item.dea!)));
- }
- break;
- // KDJ
- case SecondaryState.KDJ:
- if (item.d != null) {
- mSecondaryRectList[index].mMaxValue = max(
- mSecondaryRectList[index].mMaxValue,
- max(item.k!, max(item.d!, item.j!)));
- mSecondaryRectList[index].mMinValue = min(
- mSecondaryRectList[index].mMinValue,
- min(item.k!, min(item.d!, item.j!)));
- }
- break;
- // RSI
- case SecondaryState.RSI:
- if (item.rsi != null) {
- mSecondaryRectList[index].mMaxValue =
- max(mSecondaryRectList[index].mMaxValue, item.rsi!);
- mSecondaryRectList[index].mMinValue =
- min(mSecondaryRectList[index].mMinValue, item.rsi!);
- }
- break;
- // WR
- case SecondaryState.WR:
- mSecondaryRectList[index].mMaxValue = 0;
- mSecondaryRectList[index].mMinValue = -100;
- break;
- // CCI
- case SecondaryState.CCI:
- if (item.cci != null) {
- mSecondaryRectList[index].mMaxValue =
- max(mSecondaryRectList[index].mMaxValue, item.cci!);
- mSecondaryRectList[index].mMinValue =
- min(mSecondaryRectList[index].mMinValue, item.cci!);
- }
- break;
- // default:
- // mSecondaryRectList[index].mMaxValue = 0;
- // mSecondaryRectList[index].mMinValue = 0;
- // break;
- }
- }
- // translate x
- double xToTranslateX(double x) => -mTranslateX + x / scaleX;
- int indexOfTranslateX(double translateX) =>
- _indexOfTranslateX(translateX, 0, mItemCount - 1);
- /// Using binary search for the index of the current value
- int _indexOfTranslateX(double translateX, int start, int end) {
- if (end == start || end == -1) {
- return start;
- }
- if (end - start == 1) {
- double startValue = getX(start);
- double endValue = getX(end);
- return (translateX - startValue).abs() < (translateX - endValue).abs()
- ? start
- : end;
- }
- int mid = start + (end - start) ~/ 2;
- double midValue = getX(mid);
- if (translateX < midValue) {
- return _indexOfTranslateX(translateX, start, mid);
- } else if (translateX > midValue) {
- return _indexOfTranslateX(translateX, mid, end);
- } else {
- return mid;
- }
- }
- /// Get x coordinate based on index
- /// + mPointWidth / 2 to prevent the first and last K-line from displaying incorrectly
- /// @param position index value
- double getX(int position) => position * mPointWidth + mPointWidth / 2;
- KLineEntity getItem(int position) {
- return datas![position];
- // if (datas != null) {
- // return datas[position];
- // } else {
- // return null;
- // }
- }
- /// scrollX convert to TranslateX
- void setTranslateXFromScrollX(double scrollX) =>
- mTranslateX = scrollX + getMinTranslateX();
- /// get the minimum value of translation
- double getMinTranslateX() {
- var x = -mDataLen + mWidth / scaleX - mPointWidth / 2 - xFrontPadding;
- return x >= 0 ? 0.0 : x;
- }
- /// calculate the value of x after long pressing and convert to [index]
- int calculateSelectedX(double selectX) {
- int mSelectedIndex = indexOfTranslateX(xToTranslateX(selectX));
- if (mSelectedIndex < mStartIndex) {
- mSelectedIndex = mStartIndex;
- }
- if (mSelectedIndex > mStopIndex) {
- mSelectedIndex = mStopIndex;
- }
- return mSelectedIndex;
- }
- /// translateX is converted to X in view
- double translateXtoX(double translateX) =>
- (translateX + mTranslateX) * scaleX;
- /// define text style
- TextStyle getTextStyle(Color color) {
- return TextStyle(fontSize: 10.0, color: color);
- }
- @override
- bool shouldRepaint(BaseChartPainter oldDelegate) {
- return true;
- }
- }
- /// Render Rectangle
- class RenderRect {
- Rect mRect;
- double mMaxValue = double.minPositive, mMinValue = double.maxFinite;
- RenderRect(this.mRect);
- }
|