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

Flutter LineChart DateTime横轴设置及X轴不显示问题排查

Flutter LineChart X轴时间显示异常解决方案

问题背景

本地数据库表包含changeAmount(金额变化)和changeTime(变化时间)字段,使用Flutter的fl_chart库绘制折线图时,Y轴金额正常显示,但X轴时间无法正常展示。

现有X值提取逻辑:

List<FlSpot> spots = widget.totalChangedData.map((data) {
  DateTime changeTime = DateTime.parse(data['changeTime']);
  double xValue = changeTime.day.toDouble();

  double changeAmount = data['changeAmount'];
  return FlSpot(
    xValue,
    changeAmount / 10000,
  );
}).toList();

数据库示例数据:

[{id: 1, changeAmount: 30000.0, changeTime: 2023-12-28T08:41:24}, {id: 2, changeAmount: 29500.0, changeTime: 2023-12-28T08:42:12}]

问题原因

  1. X值范围不匹配:当前X值取日期的day(示例中为28),但图表设置的maxX=11,导致数据点超出显示范围,X轴标题无法触发渲染。
  2. X轴标题逻辑错误:bottomTitleWidgets硬编码固定数值(2、5、8)对应月份,与实际时间数据完全不匹配。
  3. 时间处理不合理:直接用day作为X值,同一天的多个数据会重叠,无法体现时间连续性。

解决方案

步骤1:重新处理X轴数据

推荐两种适配方式:

方式一:数据索引作为X值(适合已排序数据集)

如果数据已按时间升序排列,直接用索引作为X值,保证连续性:

List<FlSpot> spots = widget.totalChangedData.asMap().entries.map((entry) {
  int index = entry.key;
  Map<String, dynamic> data = entry.value;
  double changeAmount = data['changeAmount'];
  return FlSpot(
    index.toDouble(),
    changeAmount / 10000,
  );
}).toList();

方式二:时间戳作为X值(适合大时间跨度场景)

将时间转换为缩小后的时间戳,避免数值过大导致图表缩放异常:

List<FlSpot> spots = widget.totalChangedData.map((data) {
  DateTime changeTime = DateTime.parse(data['changeTime']);
  // 毫秒时间戳缩小1000倍,控制数值范围
  double xValue = changeTime.millisecondsSinceEpoch.toDouble() / 1000;
  double changeAmount = data['changeAmount'];
  return FlSpot(
    xValue,
    changeAmount / 10000,
  );
}).toList();

步骤2:修改X轴标题渲染逻辑

根据X值对应的数据,格式化显示时间文本:

Widget bottomTitleWidgets(double value, TitleMeta meta) {
  const style = TextStyle(fontWeight: FontWeight.bold, fontSize: 12, color: greyColor);
  int index = value.toInt();
  // 确保索引在有效范围内
  if (index >= 0 && index < widget.totalChangedData.length) {
    DateTime time = DateTime.parse(widget.totalChangedData[index]['changeTime']);
    // 自定义时间格式,示例为"月-日 时:分"
    String formattedTime = "${time.month}-${time.day} ${time.hour}:${time.minute.toString().padLeft(2, '0')}";
    return SideTitleWidget(
      axisSide: meta.axisSide,
      child: Text(formattedTime, style: style, textAlign: TextAlign.center),
    );
  }
  return const SideTitleWidget(
    axisSide: meta.axisSide,
    child: Text('', style: style),
  );
}

如需更优雅的时间格式化,可使用intl库:

  1. 在pubspec.yaml添加依赖:
dependencies:
  intl: ^0.18.1
  1. 修改格式化逻辑:
import 'package:intl/intl.dart';

// ...

String formattedTime = DateFormat('MM-dd HH:mm').format(time);

步骤3:调整X轴范围参数

  • 用索引作为X值时:
minX: 0,
maxX: (widget.totalChangedData.length - 1).toDouble(),
  • 用时间戳作为X值时,先计算最小/最大时间戳:
double minTimestamp = widget.totalChangedData.map((data) => DateTime.parse(data['changeTime']).millisecondsSinceEpoch.toDouble() / 1000).reduce((a, b) => a < b ? a : b);
double maxTimestamp = widget.totalChangedData.map((data) => DateTime.parse(data['changeTime']).millisecondsSinceEpoch.toDouble() / 1000).reduce((a, b) => a > b ? a : b);

再设置到LineChartData中:

minX: minTimestamp,
maxX: maxTimestamp,

步骤4:优化X轴标题显示

调整预留空间和间隔,避免标题重叠:

bottomTitles: AxisTitles(
  sideTitles: SideTitles(
    showTitles: true,
    reservedSize: 40, // 增大空间避免文本截断
    interval: 1, // 数据多时可改为2,减少标题数量
    getTitlesWidget: bottomTitleWidgets,
  ),
),

完整核心代码示例

Widget bottomTitleWidgets(double value, TitleMeta meta) {
  const style = TextStyle(fontWeight: FontWeight.bold, fontSize: 12, color: greyColor);
  int index = value.toInt();
  if (index >= 0 && index < widget.totalChangedData.length) {
    DateTime time = DateTime.parse(widget.totalChangedData[index]['changeTime']);
    String formattedTime = "${time.month}-${time.day} ${time.hour}:${time.minute.toString().padLeft(2, '0')}";
    return SideTitleWidget(
      axisSide: meta.axisSide,
      child: Text(formattedTime, style: style, textAlign: TextAlign.center),
    );
  }
  return const SideTitleWidget(
    axisSide: meta.axisSide,
    child: Text('', style: style),
  );
}

LineChartData mainData() {
  // 先对数据按时间排序
  widget.totalChangedData.sort((a, b) {
    DateTime timeA = DateTime.parse(a['changeTime']);
    DateTime timeB = DateTime.parse(b['changeTime']);
    return timeA.compareTo(timeB);
  });

  List<FlSpot> spots = widget.totalChangedData.asMap().entries.map((entry) {
    int index = entry.key;
    Map<String, dynamic> data = entry.value;
    double changeAmount = data['changeAmount'];
    return FlSpot(
      index.toDouble(),
      changeAmount / 10000,
    );
  }).toList();

  return LineChartData(
    gridData: FlGridData(
      show: false,
      drawVerticalLine: true,
      horizontalInterval: 1,
      verticalInterval: 1,
      getDrawingHorizontalLine: (value) {
        return const FlLine(
          color: greyColor,
          strokeWidth: 1,
        );
      },
      getDrawingVerticalLine: (value) {
        return const FlLine(
          color: greyColor,
          strokeWidth: 1,
        );
      },
    ),
    titlesData: FlTitlesData(
      show: true,
      rightTitles: const AxisTitles(
        sideTitles: SideTitles(showTitles: false),
      ),
      topTitles: const AxisTitles(
        sideTitles: SideTitles(showTitles: false),
      ),
      bottomTitles: AxisTitles(
        sideTitles: SideTitles(
          showTitles: true,
          reservedSize: 40,
          interval: 1,
          getTitlesWidget: bottomTitleWidgets,
        ),
      ),
      leftTitles: AxisTitles(
        sideTitles: SideTitles(
          showTitles: true,
          interval: 1,
          getTitlesWidget: leftTitleWidgets,
          reservedSize: 32,
        ),
      ),
    ),
    borderData: FlBorderData(
      show: false,
      border: Border.all(color: const Color(0xff37434d)),
    ),
    minX: 0,
    maxX: (widget.totalChangedData.length - 1).toDouble(),
    minY: 0,
    maxY: 6,
    lineBarsData: [
      LineChartBarData(
        spots: spots,
        isCurved: true,
        gradient: const LinearGradient(
          colors: gradientColors,
        ),
        barWidth: 3,
        isStrokeCapRound: true,
        dotData: const FlDotData(
          show: false,
        ),
        belowBarData: BarAreaData(
          show: true,
          gradient: LinearGradient(
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter,
            colors: gradientColors
                .map((color) => color.withOpacity(0.3))
                .toList(),
          ),
        ),
      ),
    ],
  );
}

内容的提问来源于stack exchange,提问作者Ihsan Ullah

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 09:20:08