Flutter图表范围选择器插件chart_range_selector的使用

发布于 1周前 作者 eggper 来自 Flutter

Flutter图表范围选择器插件chart_range_selector的使用

前言

最近有一个小需求:图表支持局部显示,如下底部的区域选择器支持。

  • 左右拖动调节中间区域
  • 拖拽中间区域,可以进行移动
  • 图表数据根据中间区域的占比进行显示部分数据

图表局部显示

这样当图表的数据量过大,不宜全部展示时,可选择的局部展示就是个不错的解决方案。由于一般的图表库没有提供该功能,这里自己通过绘制来实现以下,操作效果如下所示:

图表拖拽效果


1. 使用 chart_range_selector

目前这个范围选择器已经发布到 pub 上了,名字是 chart_range_selector。大家可以通过依赖进行添加。

dependencies:
  chart_range_selector: ^1.0.0

这个库本身是作为独立 UI 组件存在的,在拖拽过程中改变区域范围时,会触发回调。使用者可以通过监听来获取当前区域的范围。这里的区域起止是以分率的形式给出的,也就是最左侧是 0 最右侧是 1 。如下的区域范围是 0.26 ~ 0.72

范围选择器示意图

ChartRangeSelector(
  height: 30,
  initStart: 0.4,
  initEnd: 0.6,
  onChartRangeChange: _onChartRangeChange,
),

void _onChartRangeChange(double start, double end) {
  print("start:$start, end:$end");
}

2. ChartRangeSelector 实现思路分析

这个组件整体上是通过 ChartRangeSelectorPainter 绘制出来的,其实这些图形都是挺规整的,绘制来说并不是什么难事。重点在于事件的处理,拖拽不同的部位需要处理不同的逻辑,还涉及对拖拽部位的校验、高亮示意,对这块的整合还是需要一定的功力的。

绘制示意图

代码中通过 RangeData 可监听对象为绘制提供必要的数据,其中 minGap 用于控制范围的最小值,保证范围不会过小。另外定义了 OperationType 枚举表示操作,其中有四个元素,none 表示没有拖拽的普通状态;dragHead 表示拖动起始块,dragTail 表示拖动终止块,dragZone 表示拖动范围区域。

enum OperationType{
  none,
  dragHead,
  dragTail,
  dragZone
}

class RangeData extends ChangeNotifier {
  double start;
  double end;
  double minGap;
  OperationType operationType=OperationType.none;

  RangeData({this.start = 0, this.end = 1,this.minGap=0.1});
  
  //暂略相关方法...
}

在组件构建中,通过 LayoutBuilder 获取组件的约束信息,从而获得约束区域宽度最大值,也就是说组件区域的宽度值由使用者自行约束,该组件并不强制指定。使用 SizedBox 限定画板的高度,通过 CustomPaint 组件使用 ChartRangeSelectorPainter 进行绘制。使用 GestureDetector 组件进行手势交互监听,这就是该组件整体上实现的思路。

组件结构图


3. 核心代码实现分析

可以看出,这个组件的核心就是 绘制 + 手势交互 。其中绘制比较简单,就是根据 RangeData 数据和颜色配置画些方块而已,稍微困难一点的是对左右控制柄位置的计算。另外,三个可拖拽物的激活状态是通过 RangeData#operationType 进行判断的。

也就是说所有问题的焦点都集中在 手势交互 中对 RangeData 数据的更新。如下是处理按下的逻辑,当触电横坐标左右 10 逻辑像素之内,表示激活头部。如下 tag1 处通过 dragHead 方法更新 operationType 并触发通知,这样画板绘制时就会激活头部块,右侧和中间的激活同理。

---->[RangeData#dragHead]----
void dragHead(){
  operationType=OperationType.dragHead;
  notifyListeners();
}

对于拖手势的处理,是比较复杂的。如下根据 operationType 进行不同的逻辑处理,比如当 dragHead 时,触发 RangeData#moveHead 方法移动 start 值。这里将具体地逻辑封装在 RangeData 类中。可以使代码更加简洁明了,每个操作都有 bool 返回值用于校验区域也没有发生变化,比如拖拽到 0 时,继续拖拽是会触发事件的,此时返回 false,避免无意义的 onChartRangeChange 回调触发。

void _onUpdate(DragUpdateDetails details, double width) {
  bool changed = false;
  if (rangeData.operationType == OperationType.dragHead) {
    changed = rangeData.moveHead(details.delta.dx / width);
  }
  if (rangeData.operationType == OperationType.dragTail) {
    changed = rangeData.moveTail(details.delta.dx / width);
  }
  if (rangeData.operationType == OperationType.dragZone) {
    changed = rangeData.move(details.delta.dx / width);
  }
  if (changed) widget.onChartRangeChange.call(rangeData.start, rangeData.end);
}

如下是 RangeData#moveHead 的处理逻辑,_recordStart 用于记录起始值,如果移动后未改变,返回 false。表示不执行通知和触发回调。

---->[RangeData#moveHead]----
bool moveHead(double ds) {
  start += ds;
  start = start.clamp(0, end - minGap);
  if (start == _recordStart) return false;
  _recordStart = start;
  notifyListeners();
  return true;
}

4. 结合图表使用

下面是结合 charts_flutter 图标库实现的范围显示案例。其中核心点是 domainAxis 可以通过 NumericAxisSpec 来显示某个范围的数据,而 ChartRangeSelector 提供拽的交互操作来更新这个范围,可谓相辅相成。

结合图表显示

class RangeChartDemo extends StatefulWidget {
  const RangeChartDemo({Key? key}) : super(key: key);

  [@override](/user/override)
  State<RangeChartDemo> createState() => _RangeChartDemoState();
}

class _RangeChartDemoState extends State<RangeChartDemo> {
  List<ChartData> data = [];

  int start = 0;
  int end = 0;

  [@override](/user/override)
  void initState() {
    super.initState();
    data = randomDayData(count: 96);
    start = 0;
    end = (0.8 * data.length).toInt();
  }

  Random random = Random();

  List<ChartData> randomDayData({int count = 1440}) {
    return List.generate(count, (index) {
      int value = 50 + random.nextInt(200);
      return ChartData(index, value);
    });
  }

  [@override](/user/override)
  Widget build(BuildContext context) {

    List<charts.Series<ChartData, int>> seriesList = [
      charts.Series<ChartData, int>(
        id: 'something',
        colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault,
        domainFn: (ChartData sales, _) => sales.index,
        measureFn: (ChartData sales, _) => sales.value,
        data: data,
      )
    ];

    return Column(
      children: [
        Expanded(
          child: charts.LineChart(seriesList,
              animate: false,
              primaryMeasureAxis: const charts.NumericAxisSpec(
                  tickProviderSpec: charts.BasicNumericTickProviderSpec(desiredTickCount: 5),),
              domainAxis: charts.NumericAxisSpec(
                viewport: charts.NumericExtents(start, end),
              )),
        ),
        const SizedBox(
          height: 10,
        ),
        SizedBox(
          width: 400,
          child: ChartRangeSelector(
              height: 30,
              initEnd: 0.5,
              initStart: 0.3,
              onChartRangeChange: (start, end) {
                this.start = (start * data.length).toInt();
                this.end = (end * data.length).toInt();
                setState(() {});
              }),
        ),
      ],
    );
  }
}

class ChartData {
  final int index;
  final int value;

  ChartData(this.index, this.value);
}

更多关于Flutter图表范围选择器插件chart_range_selector的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter图表范围选择器插件chart_range_selector的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,下面是一个关于如何在Flutter中使用chart_range_selector插件的示例代码。这个插件通常用于在图表上实现范围选择器功能,以便用户可以动态地选择图表的一部分进行查看。

首先,确保你已经在pubspec.yaml文件中添加了chart_range_selector依赖:

dependencies:
  flutter:
    sdk: flutter
  chart_range_selector: ^x.y.z  # 请替换为最新版本号

然后,运行flutter pub get来安装依赖。

以下是一个完整的示例代码,展示如何在一个Flutter应用中集成并使用chart_range_selector插件:

import 'package:flutter/material.dart';
import 'package:charts_flutter/flutter_charts.dart' as charts;
import 'package:chart_range_selector/chart_range_selector.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: ChartRangeSelectorExample(),
    );
  }
}

class ChartRangeSelectorExample extends StatefulWidget {
  @override
  _ChartRangeSelectorExampleState createState() => _ChartRangeSelectorExampleState();
}

class _ChartRangeSelectorExampleState extends State<ChartRangeSelectorExample> {
  final List<charts.Series<MyData, num>> _series = [
    charts.Series<MyData, num>(
      id: 'Data',
      data: createSampleData(),
      domainFn: (MyData data, _) => data.x,
      measureFn: (MyData data, _) => data.y,
      color: charts.MaterialPalette.blue.shadeDefault,
    ),
  ];

  DateTimeRange? _selectedRange;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Chart Range Selector Example'),
      ),
      body: Column(
        children: [
          Expanded(
            child: ChartRangeSelector<MyData>(
              series: _series,
              initialRange: DateTimeRange(
                start: DateTime(2021, 1, 1),
                end: DateTime(2021, 12, 31),
              ),
              onRangeSelected: (DateTimeRange range) {
                setState(() {
                  _selectedRange = range;
                });
              },
            ),
          ),
          if (_selectedRange != null)
            Padding(
              padding: const EdgeInsets.all(16.0),
              child: Text(
                'Selected Range: ${_selectedRange!.start.toLocal().toDateString()} - ${_selectedRange!.end.toLocal().toDateString()}',
              ),
            ),
        ],
      ),
    );
  }

  List<MyData> createSampleData() {
    final List<MyData> data = [];
    for (int i = 0; i < 365; i++) {
      final DateTime date = DateTime(2021, 1, 1).add(Duration(days: i));
      data.add(MyData(date, i.toDouble()));
    }
    return data;
  }
}

class MyData {
  final DateTime x;
  final double y;

  MyData(this.x, this.y);
}

关键点解释:

  1. 依赖添加:在pubspec.yaml文件中添加chart_range_selectorcharts_flutter(如果你使用Flutter Charts库)的依赖。

  2. 数据模型:定义了一个MyData类来表示图表数据,其中包含日期(DateTime)和值(double)。

  3. 创建数据:在createSampleData方法中生成了一年的示例数据。

  4. 图表和范围选择器

    • 使用ChartRangeSelector组件,将系列数据(_series)和初始范围(initialRange)传递给它。
    • 监听onRangeSelected回调,当用户选择范围时更新状态。
  5. 显示选择范围:如果选择了范围,则在图表下方显示所选范围的日期。

请注意,chart_range_selector插件的具体用法可能会根据版本有所不同,因此请参考其官方文档和示例代码以获得最新和最准确的信息。如果chart_range_selector插件的API有变化,可能需要调整上述代码以适应新版本。

回到顶部