k_chart_multiple 1.4.1 copy "k_chart_multiple: ^1.4.1" to clipboard
k_chart_multiple: ^1.4.1 copied to clipboard

A Flutter K Chart support multiple secondary chart.

example/lib/main.dart

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:http/http.dart' as http;
import 'package:k_chart_multiple/chart_translations.dart';
import 'package:k_chart_multiple/flutter_k_chart.dart';

void main() => runApp(const MyApp());

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  static const List<Locale> _localeCycle = <Locale>[
    Locale('en', 'US'),
    Locale('zh', 'CN'),
    Locale('es', 'ES'),
    Locale('ja', 'JP'),
  ];

  Locale _locale = _localeCycle.first;

  void _setLocale(Locale locale) {
    setState(() {
      _locale = locale;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(primarySwatch: Colors.green),
      locale: _locale,
      supportedLocales: _localeCycle,
      localizationsDelegates: const [
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      home: MyHomePage(
        title: 'Flutter Demo Home Page',
        locale: _locale,
        locales: _localeCycle,
        onLocaleChanged: _setLocale,
      ),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage(
      {super.key,
      required this.title,
      required this.locale,
      required this.locales,
      required this.onLocaleChanged});

  final String title;
  final Locale locale;
  final List<Locale> locales;
  final ValueChanged<Locale> onLocaleChanged;

  @override
  MyHomePageState createState() => MyHomePageState();
}

class MyHomePageState extends State<MyHomePage> {
  List<KLineEntity>? datas;
  bool showLoading = true;
  MainState _mainState = MainState.MA;
  bool _volHidden = false;
  final List<SecondaryState> _secondaryStates = const [
    SecondaryState.KDJ,
    SecondaryState.CMF,
    SecondaryState.CHAIKIN_OSC,
    SecondaryState.KLINGER,
  ].toList();
  bool isLine = false;
  bool _hideGrid = false;
  bool _showNowPrice = true;
  List<DepthEntity>? _bids, _asks;
  bool isChangeUI = false;
  bool _isTrendLine = false;
  bool _priceLeft = true;
  VerticalTextAlignment _verticalTextAlignment = VerticalTextAlignment.left;

  ChartStyle chartStyle = ChartStyle();
  ChartColors chartColors = ChartColors();

  static const Map<String, ChartTranslations> _translations = {
    'en_US': ChartTranslations(),
    ...kChartTranslations,
  };

  static const List<SecondaryState> _newIndicatorStates = [
    SecondaryState.CMF,
    SecondaryState.CHAIKIN_OSC,
    SecondaryState.KLINGER,
    SecondaryState.VPT,
    SecondaryState.FORCE,
    SecondaryState.ROC,
    SecondaryState.ULTIMATE,
    SecondaryState.CONNORS_RSI,
    SecondaryState.STOCH_RSI,
    SecondaryState.RVI,
    SecondaryState.DPO,
    SecondaryState.KAMA,
    SecondaryState.HMA,
    SecondaryState.KELTNER,
    SecondaryState.DONCHIAN,
    SecondaryState.BOLL_BANDWIDTH,
    SecondaryState.CHAIKIN_VOLATILITY,
    SecondaryState.HV_PERCENTILE,
    SecondaryState.ATR_PERCENTILE,
    SecondaryState.ELDER_RAY,
    SecondaryState.ICHIMOKU_SPAN,
    SecondaryState.PIVOT,
    SecondaryState.GANN_FAN,
    SecondaryState.SUPER_TREND,
    SecondaryState.STC,
    SecondaryState.QQE,
    SecondaryState.WAVE_TREND,
    SecondaryState.CMO,
    SecondaryState.EOM,
    SecondaryState.PVI_NVI,
  ];

  static const Map<SecondaryState, Map<String, String>>
      _secondaryStateDisplayNames = {
    SecondaryState.CMF: {
      'en': 'Chaikin Money Flow',
      'zh': '资金流量(CMF)',
    },
    SecondaryState.CHAIKIN_OSC: {
      'en': 'Chaikin Oscillator',
      'zh': 'Chaikin 振荡',
    },
    SecondaryState.KLINGER: {
      'en': 'Klinger Volume Osc.',
      'zh': 'Klinger 量振荡',
    },
    SecondaryState.VPT: {
      'en': 'Volume Price Trend',
      'zh': '量价趋势(VPT)',
    },
    SecondaryState.FORCE: {
      'en': 'Force Index',
      'zh': '能量指标',
    },
    SecondaryState.ROC: {
      'en': 'Rate of Change',
      'zh': '变动率(ROC)',
    },
    SecondaryState.ULTIMATE: {
      'en': 'Ultimate Oscillator',
      'zh': '终极振荡',
    },
    SecondaryState.CONNORS_RSI: {
      'en': 'Connors RSI',
      'zh': 'Connors RSI',
    },
    SecondaryState.STOCH_RSI: {
      'en': 'Stochastic RSI',
      'zh': '随机RSI',
    },
    SecondaryState.RVI: {
      'en': 'Relative Vigor Index',
      'zh': '相对活力指数(RVI)',
    },
    SecondaryState.DPO: {
      'en': 'Detrended Price Osc.',
      'zh': '去趋势振荡(DPO)',
    },
    SecondaryState.KAMA: {
      'en': 'Kaufman Adaptive MA',
      'zh': '卡夫曼自适应MA',
    },
    SecondaryState.HMA: {
      'en': 'Hull Moving Avg.',
      'zh': '赫尔均线(HMA)',
    },
    SecondaryState.KELTNER: {
      'en': 'Keltner Channel',
      'zh': '肯特纳通道',
    },
    SecondaryState.DONCHIAN: {
      'en': 'Donchian Channel',
      'zh': '唐奇安通道',
    },
    SecondaryState.BOLL_BANDWIDTH: {
      'en': 'Bollinger Bandwidth',
      'zh': '布林带宽度',
    },
    SecondaryState.CHAIKIN_VOLATILITY: {
      'en': 'Chaikin Volatility',
      'zh': 'Chaikin 波动',
    },
    SecondaryState.HV_PERCENTILE: {
      'en': 'HV Percentile',
      'zh': '历史波动百分位',
    },
    SecondaryState.ATR_PERCENTILE: {
      'en': 'ATR Percentile',
      'zh': 'ATR 百分位',
    },
    SecondaryState.ELDER_RAY: {
      'en': 'Elder Ray',
      'zh': '艾尔德射线',
    },
    SecondaryState.ICHIMOKU_SPAN: {
      'en': 'Ichimoku Span Δ',
      'zh': '一目云差值',
    },
    SecondaryState.PIVOT: {
      'en': 'Pivot Levels',
      'zh': '枢轴点',
    },
    SecondaryState.GANN_FAN: {
      'en': 'Gann Fan',
      'zh': '江恩扇形',
    },
    SecondaryState.SUPER_TREND: {
      'en': 'SuperTrend',
      'zh': '超级趋势',
    },
    SecondaryState.STC: {
      'en': 'Schaff Trend Cycle',
      'zh': 'STC 指标',
    },
    SecondaryState.QQE: {
      'en': 'QQE',
      'zh': 'QQE 指标',
    },
    SecondaryState.WAVE_TREND: {
      'en': 'WaveTrend Oscillator',
      'zh': '波动趋势',
    },
    SecondaryState.CMO: {
      'en': 'Chande Momentum Osc.',
      'zh': '钱德动量',
    },
    SecondaryState.EOM: {
      'en': 'Ease of Movement',
      'zh': '简易波动',
    },
    SecondaryState.PVI_NVI: {
      'en': 'PVI & NVI',
      'zh': '正/负量指标',
    },
  };

  @override
  void initState() {
    super.initState();
    getData();
    rootBundle.loadString('assets/depth.json').then((result) {
      final parseJson = json.decode(result);
      final tick = parseJson['tick'] as Map<String, dynamic>;
      final List<DepthEntity> bids = (tick['bids'] as List<dynamic>)
          .map<DepthEntity>(
              (item) => DepthEntity(item[0] as double, item[1] as double))
          .toList();
      final List<DepthEntity> asks = (tick['asks'] as List<dynamic>)
          .map<DepthEntity>(
              (item) => DepthEntity(item[0] as double, item[1] as double))
          .toList();
      initDepth(bids, asks);
    });
  }

  void initDepth(List<DepthEntity>? bids, List<DepthEntity>? asks) {
    if (bids == null || asks == null || bids.isEmpty || asks.isEmpty) return;

    final processedBids = <DepthEntity>[];
    final processedAsks = <DepthEntity>[];

    double amount = 0.0;
    final sortedBids = [...bids]
      ..sort((left, right) => left.price.compareTo(right.price));
    // 累加买入委托量
    for (final item in sortedBids.reversed) {
      amount += item.vol;
      processedBids.insert(0, DepthEntity(item.price, amount));
    }

    amount = 0.0;
    final sortedAsks = [...asks]
      ..sort((left, right) => left.price.compareTo(right.price));
    // 累加卖出委托量
    for (final item in sortedAsks) {
      amount += item.vol;
      processedAsks.add(DepthEntity(item.price, amount));
    }

    if (!mounted) return;
    setState(() {
      _bids = processedBids;
      _asks = processedAsks;
    });
  }

  @override
  Widget build(BuildContext context) {
    final localeTag =
        '${widget.locale.languageCode}_${widget.locale.countryCode}';
    final double chartHeight =
        400 + 80 + 10 + _secondaryStates.length * (80 + 13);

    return ListView(
      shrinkWrap: true,
      children: <Widget>[
        Padding(
          padding: const EdgeInsets.symmetric(vertical: 12),
          child: Text(
            widget.title,
            textAlign: TextAlign.center,
            style: Theme.of(context).textTheme.titleMedium,
          ),
        ),
        Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
          child: Material(
            type: MaterialType.transparency,
            child: Row(
              mainAxisAlignment: MainAxisAlignment.end,
              mainAxisSize: MainAxisSize.min,
              children: [
                Text(
                  _localizedLabel('Language', '语言', localeTag),
                  style: Theme.of(context).textTheme.bodyMedium,
                ),
                const SizedBox(width: 12),
                DropdownButtonHideUnderline(
                  child: DropdownButton<Locale>(
                    value: widget.locale,
                    isDense: true,
                    onChanged: (Locale? value) {
                      if (value != null) {
                        widget.onLocaleChanged(value);
                      }
                    },
                    items: widget.locales
                        .map(
                          (locale) => DropdownMenuItem<Locale>(
                            value: locale,
                            child: Text(_localeDisplayName(locale)),
                          ),
                        )
                        .toList(),
                  ),
                ),
              ],
            ),
          ),
        ),
        Stack(children: <Widget>[
          SizedBox(
            height: chartHeight,
            width: double.infinity,
            child: KChartWidget(
              datas,
              chartStyle,
              chartColors,
              isLine: isLine,
              isTrendLine: _isTrendLine,
              mainState: _mainState,
              volHidden: _volHidden,
              secondaryStates: _secondaryStates,
              fixedLength: 2,
              timeFormat: TimeFormat.YEAR_MONTH_DAY,
              translations: _translations,
              showNowPrice: _showNowPrice,
              hideGrid: _hideGrid,
              isTapShowInfoDialog: false,
              verticalTextAlignment: _verticalTextAlignment,
              maDayList: const [1, 100, 1000],
              mainHeight: 400,
              secondaryHeight: 80,
              onUpProbs: (report) {
                debugPrint('The comprehensive possibility is $report');
              },
              onGoingUp: (probability) {
                debugPrint('Secondary chart rising probability: $probability');
              },
              onGoingDown: (probability) {
                debugPrint('Secondary chart falling probability: $probability');
              },
              onMainGoingUp: (probability) {
                debugPrint('Main chart rising probability: $probability');
              },
              onMainGoingDown: (probability) {
                debugPrint('Main chart falling probability: $probability');
              },
            ),
          ),
          if (showLoading)
            const SizedBox(
              width: double.infinity,
              height: 450,
              child: Center(child: CircularProgressIndicator()),
            ),
        ]),
        buildButtons(localeTag),
        if (_bids != null && _asks != null)
          SizedBox(
            height: 230,
            width: double.infinity,
            child: DepthChart(_bids!, _asks!, chartColors),
          )
      ],
    );
  }

  Widget buildButtons(String localeTag) {
    final Set<SecondaryState> newIndicatorSet = _newIndicatorStates.toSet();
    final List<SecondaryState> classicIndicators = SecondaryState.values
        .where((state) =>
            state != SecondaryState.NONE && !newIndicatorSet.contains(state))
        .toList();

    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        Wrap(
          alignment: WrapAlignment.spaceEvenly,
          children: <Widget>[
            button(_localizedLabel('Time Mode', '分时', localeTag),
                selected: isLine, onPressed: () => isLine = true),
            button(_localizedLabel('K Line Mode', 'K线模式', localeTag),
                selected: !isLine, onPressed: () => isLine = false),
            button(_localizedLabel('TrendLine', '画趋势线', localeTag),
                selected: _isTrendLine,
                onPressed: () => _isTrendLine = !_isTrendLine),
            button(_localizedLabel('Line:MA', '主图:MA', localeTag),
                selected: _mainState == MainState.MA,
                onPressed: () => _mainState = MainState.MA),
            button(_localizedLabel('Line:BOLL', '主图:BOLL', localeTag),
                selected: _mainState == MainState.BOLL,
                onPressed: () => _mainState = MainState.BOLL),
            button(_localizedLabel('Hide Line', '主图:隐藏', localeTag),
                selected: _mainState == MainState.NONE,
                onPressed: () => _mainState = MainState.NONE),
            button(_localizedLabel('Hide Secondary', '清空副图', localeTag),
                selected: _secondaryStates.isEmpty,
                onPressed: () => _secondaryStates.clear()),
            button(
                _volHidden
                    ? _localizedLabel('Show Vol', '显示成交量', localeTag)
                    : _localizedLabel('Hide Vol', '隐藏成交量', localeTag),
                selected: _volHidden,
                onPressed: () => _volHidden = !_volHidden),
            button(
                _hideGrid
                    ? _localizedLabel('Show Grid', '显示网格', localeTag)
                    : _localizedLabel('Hide Grid', '隐藏网格', localeTag),
                selected: _hideGrid,
                onPressed: () => _hideGrid = !_hideGrid),
            button(
                _showNowPrice
                    ? _localizedLabel('Hide Now Price', '隐藏最新价', localeTag)
                    : _localizedLabel('Show Now Price', '显示最新价', localeTag),
                selected: !_showNowPrice,
                onPressed: () => _showNowPrice = !_showNowPrice),
            button(_localizedLabel('Customize UI', '自定义样式', localeTag),
                selected: isChangeUI, onPressed: () {
              isChangeUI = !isChangeUI;
              if (isChangeUI) {
                chartColors.selectBorderColor = Colors.red;
                chartColors.selectFillColor = Colors.red;
                chartColors.lineFillColor = Colors.red;
                chartColors.kLineColor = Colors.yellow;
              } else {
                chartColors.selectBorderColor = const Color(0xff6C7A86);
                chartColors.selectFillColor = const Color(0xff0D1722);
                chartColors.lineFillColor = const Color(0x554C86CD);
                chartColors.kLineColor = const Color(0xff4C86CD);
              }
            }),
            button(_localizedLabel('Toggle Price Label', '切换价位文字', localeTag),
                selected: !_priceLeft, onPressed: () {
              _priceLeft = !_priceLeft;
              _verticalTextAlignment = _priceLeft
                  ? VerticalTextAlignment.left
                  : VerticalTextAlignment.right;
            }),
          ],
        ),
        const SizedBox(height: 20),
        Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16.0),
          child: Text(
            _localizedLabel('Classic Secondary Indicators', '经典副图', localeTag),
            style: Theme.of(context).textTheme.titleSmall,
          ),
        ),
        const SizedBox(height: 8),
        Wrap(
          alignment: WrapAlignment.start,
          children: _buildSecondaryButtons(classicIndicators, localeTag),
        ),
        const SizedBox(height: 24),
        Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16.0),
          child: Text(
            _localizedLabel('New Secondary Indicators', '新增副图', localeTag),
            style: Theme.of(context)
                .textTheme
                .titleSmall
                ?.copyWith(color: Colors.orangeAccent),
          ),
        ),
        const SizedBox(height: 8),
        Wrap(
          alignment: WrapAlignment.start,
          children: _buildSecondaryButtons(_newIndicatorStates, localeTag),
        ),
      ],
    );
  }

  List<Widget> _buildSecondaryButtons(
      List<SecondaryState> states, String localeTag) {
    return states.map((state) {
      final label = _secondaryStateName(state, localeTag);
      return button(
        label,
        selected: _secondaryStates.contains(state),
        onPressed: () {
          if (_secondaryStates.contains(state)) {
            _secondaryStates.remove(state);
          } else {
            _secondaryStates.add(state);
          }
        },
      );
    }).toList();
  }

  String _secondaryStateName(SecondaryState state, String localeTag) {
    final labels = _secondaryStateDisplayNames[state];
    if (labels == null) {
      return state.toString().split('.').last;
    }
    if (localeTag == 'zh_CN') {
      return labels['zh'] ?? labels['en'] ?? state.toString().split('.').last;
    }
    return labels['en'] ?? state.toString().split('.').last;
  }

  Widget button(String text, {VoidCallback? onPressed, bool selected = false}) {
    final Color backgroundColor = selected ? Colors.orange : Colors.blue;
    return TextButton(
      onPressed: onPressed == null
          ? null
          : () {
              onPressed();
              setState(() {});
            },
      style: TextButton.styleFrom(
        foregroundColor: Colors.white,
        minimumSize: const Size(88, 44),
        padding: const EdgeInsets.symmetric(horizontal: 16.0),
        shape: const RoundedRectangleBorder(
          borderRadius: BorderRadius.all(Radius.circular(2.0)),
        ),
        backgroundColor: backgroundColor,
      ),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          if (selected) ...[
            const Icon(Icons.check, size: 16, color: Colors.white),
            const SizedBox(width: 6),
          ],
          Text(text),
        ],
      ),
    );
  }

  String _localizedLabel(String en, String zh, String localeTag) {
    switch (localeTag) {
      case 'zh_CN':
        return zh;
      case 'es_ES':
        return _spanishLabels[en] ?? en;
      case 'ja_JP':
        return _japaneseLabels[en] ?? en;
      default:
        return en;
    }
  }

  static const Map<String, String> _spanishLabels = {
    'Time Mode': 'Modo tiempo',
    'K Line Mode': 'Modo velas',
    'TrendLine': 'Línea de tendencia',
    'Line:MA': 'Principal: MA',
    'Line:BOLL': 'Principal: BOLL',
    'Hide Line': 'Ocultar principal',
    'Hide Secondary': 'Ocultar secundarios',
    'Show Vol': 'Mostrar volumen',
    'Hide Vol': 'Ocultar volumen',
    'Change Language': 'Cambiar idioma',
    'Hide Grid': 'Ocultar rejilla',
    'Show Grid': 'Mostrar rejilla',
    'Hide Now Price': 'Ocultar precio actual',
    'Show Now Price': 'Mostrar precio actual',
    'Customize UI': 'Personalizar UI',
    'Toggle Price Label': 'Cambiar etiqueta de precio',
    'Language': 'Idioma',
  };

  static const Map<String, String> _japaneseLabels = {
    'Time Mode': '時間足',
    'K Line Mode': 'ローソク足',
    'TrendLine': 'トレンドライン',
    'Line:MA': '主図:MA',
    'Line:BOLL': '主図:BOLL',
    'Hide Line': '主図:非表示',
    'Hide Secondary': '副図を消去',
    'Show Vol': '出来高を表示',
    'Hide Vol': '出来高を隠す',
    'Change Language': '言語を切替',
    'Hide Grid': 'グリッドを隠す',
    'Show Grid': 'グリッドを表示',
    'Hide Now Price': '現在値を隠す',
    'Show Now Price': '現在値を表示',
    'Customize UI': 'UIをカスタム',
    'Toggle Price Label': '価格ラベル切替',
    'Language': '言語',
  };

  String _localeDisplayName(Locale locale) {
    final tag = '${locale.languageCode}_${locale.countryCode}';
    switch (tag) {
      case 'zh_CN':
        return '简体中文';
      case 'es_ES':
        return 'Español';
      case 'ja_JP':
        return '日本語';
      default:
        return 'English';
    }
  }

  Future<void> getData() async {
    try {
      final result = await getChatDataFromJson();
      if (!mounted) return;
      solveChatData(result);
    } catch (error) {
      if (!mounted) {
        debugPrint('Failed to load chart data: $error');
        return;
      }
      setState(() {
        showLoading = false;
      });
      debugPrint('Failed to load chart data: $error');
    }
  }

  //获取火币数据,需要翻墙
  Future<String> getChatDataFromInternet(String? period) async {
    final url =
        'https://api.huobi.br.com/market/history/kline?period=${period ?? '1day'}&size=300&symbol=btcusdt';
    final response = await http.get(Uri.parse(url));
    if (response.statusCode == 200) {
      return response.body;
    }
    throw Exception(
        'Failed fetching remote market data (${response.statusCode})');
  }

  // 如果你不能翻墙,可以使用这个方法加载数据
  Future<String> getChatDataFromJson() async {
    return rootBundle.loadString('assets/chatData.json');
  }

  void solveChatData(String result) {
    final Map<String, dynamic> parseJson =
        json.decode(result) as Map<String, dynamic>;
    final rawList = (parseJson['data'] as List<dynamic>)
        .map((item) => KLineEntity.fromJson(item as Map<String, dynamic>))
        .toList()
        .reversed
        .toList();
    final parsedData = rawList.cast<KLineEntity>();
    DataUtil.calculate(parsedData);
    if (!mounted) return;
    setState(() {
      datas = parsedData;
      showLoading = false;
    });
  }
}
0
likes
140
points
186
downloads

Publisher

unverified uploader

Weekly Downloads

A Flutter K Chart support multiple secondary chart.

Repository (GitHub)
View/report issues

Documentation

API reference

License

BSD-2-Clause (license)

Dependencies

flutter, flutter_localizations

More

Packages that depend on k_chart_multiple