You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Flutter中复现带时间轴的多层堆叠柱状图?

解决方案建议

一、更合适的Flutter图表包

如果Syncfusion和fl_chart满足不了你的定制需求,可以试试这两个选项:

  • charts_flutter(谷歌官方维护):原生支持堆叠柱状图,对时间轴适配性强,能自定义X轴的时间格式(比如11:12 PM这种),图例也支持自定义样式。基础的点击、tooltip交互都有,适合快速搭建接近需求的图表。
  • flutter_echarts:基于ECharts封装,几乎能实现所有ECharts的效果,包括复杂的堆叠柱状图、带时长占比的自定义图例、个性化交互逻辑。你可以直接用ECharts的配置项匹配设计,上手成本主要是熟悉ECharts的配置语法。

二、CustomPainter是否更优?

如果你的设计有非常独特的视觉细节(比如特殊的柱子圆角、渐变效果、自定义交互反馈),现有包无法实现,那CustomPainter肯定是最优解——完全掌控绘制逻辑。但要注意:

  • 开发成本高:需要自己处理坐标转换、堆叠值计算、事件监听(点击、hover)、图例联动等细节。
  • 性能优化:数据量大时,要注意缓存绘制对象,避免频繁重绘。
  • 交互实现:可以结合GestureDetector实现点击柱子显示详情、滑动时间轴等交互。

三、时间型数据的高效管理

  1. 数据标准化:把所有时间字符串转成DateTime对象,方便排序和计算时间差:
final timeStr = "11:12 PM";
final dateTime = DateFormat("hh:mm a").parse(timeStr);
  1. 数据预处理:
    • 合并连续相同状态的时间段:比如同一状态在10:00-10:30和10:30-11:00连续,就合并成10:00-11:00,减少柱子数量,提升绘制效率。
    • 计算各状态总时长和占比:遍历所有数据,累加各状态的时长,再除以总时长得到占比,用于底部图例展示。
  2. 时间轴刻度生成:根据数据的时间范围,自动生成合适的刻度间隔(比如每小时一个标签,或者每30分钟),避免标签重叠。可以用intl包的DateFormat格式化显示。

代码片段示例

1. charts_flutter实现基础堆叠柱状图

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

class StatusData {
  final DateTime time;
  final int low;
  final int medium;
  final int high;

  StatusData(this.time, this.low, this.medium, this.high);
}

class StackedBarChart extends StatelessWidget {
  final List<StatusData> data;

  StackedBarChart(this.data);

  @override
  Widget build(BuildContext context) {
    final series = [
      charts.Series<StatusData, DateTime>(
        id: 'Low',
        colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault,
        domainFn: (StatusData data, _) => data.time,
        measureFn: (StatusData data, _) => data.low,
        data: data,
      ),
      charts.Series<StatusData, DateTime>(
        id: 'Medium',
        colorFn: (_, __) => charts.MaterialPalette.yellow.shadeDefault,
        domainFn: (StatusData data, _) => data.time,
        measureFn: (StatusData data, _) => data.medium,
        data: data,
      ),
      charts.Series<StatusData, DateTime>(
        id: 'High',
        colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault,
        domainFn: (StatusData data, _) => data.time,
        measureFn: (StatusData data, _) => data.high,
        data: data,
      ),
    ];

    return charts.TimeSeriesChart(
      series,
      animate: true,
      barGroupingType: charts.BarGroupingType.stacked,
      domainAxis: charts.DateTimeAxisSpec(
        tickFormatterSpec: charts.AutoDateTimeTickFormatterSpec(
          hour: charts.TimeFormatterSpec(
            format: 'hh:mm a',
            transitionFormat: 'hh:mm a',
          ),
        ),
      ),
      behaviors: [
        charts.SeriesLegend(
          position: charts.BehaviorPosition.bottom,
          entryTextStyle: charts.TextStyleSpec(
            fontSize: 12,
            color: charts.MaterialPalette.black,
          ),
        ),
      ],
    );
  }
}

2. 时间数据预处理示例

import 'package:intl/intl.dart';

// 原始数据结构
class RawStatusRecord {
  final String startTime;
  final String endTime;
  final String status;

  RawStatusRecord(this.startTime, this.endTime, this.status);
}

// 合并连续相同状态的时间段
List<RawStatusRecord> mergeContinuousStatus(List<RawStatusRecord> records) {
  if (records.isEmpty) return [];
  
  final merged = <RawStatusRecord>[];
  var current = records.first;
  
  for (var i = 1; i < records.length; i++) {
    final next = records[i];
    final currentEnd = DateFormat("hh:mm a").parse(current.endTime);
    final nextStart = DateFormat("hh:mm a").parse(next.startTime);
    
    if (current.status == next.status && currentEnd == nextStart) {
      current = RawStatusRecord(current.startTime, next.endTime, current.status);
    } else {
      merged.add(current);
      current = next;
    }
  }
  merged.add(current);
  
  return merged;
}

// 计算各状态总时长和占比
Map<String, Map<String, dynamic>> calculateStatusStats(List<RawStatusRecord> records) {
  final stats = <String, int>{};
  var totalDuration = 0;
  
  for (var record in records) {
    final start = DateFormat("hh:mm a").parse(record.startTime);
    final end = DateFormat("hh:mm a").parse(record.endTime);
    final duration = end.difference(start).inMinutes;
    
    stats[record.status] = (stats[record.status] ?? 0) + duration;
    totalDuration += duration;
  }
  
  return stats.map((key, value) => MapEntry(
    key,
    {
      'duration': '$value 分钟',
      'percentage': ((value / totalDuration) * 100).toStringAsFixed(1) + '%'
    }
  ));
}

总结

  • 优先尝试flutter_echarts,配置灵活,能快速匹配复杂设计;
  • 若定制需求极高,再考虑CustomPainter,做好性能优化;
  • 时间数据一定要标准化处理,预处理能减少绘制压力,提升交互流畅度。

内容的提问来源于stack exchange,提问作者Akinchan D Roger

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 11:20:17