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

Flutter中fl_chart(v0.70)柱状图显示异常问题求助

解决fl_chart v0.70柱状图溢出与轴标签不显示问题

问题描述

  • 柱状图超出容器范围,高度撑满当前窗口
  • 无任何轴标签显示,即使保留了示例中的标签代码
  • 直接复制示例代码(仅替换AppColors为Colors、移除AppUtils)时,图表不会溢出但仍无标签

问题代码

class BarChartSample5 extends StatefulWidget {
  List<Expenditure> expenditures;
  bool displayMonthly;
  DateTime date;
  bool displayAverage;
  BarChartSample5(
      {super.key,
      required this.expenditures,
      required this.displayMonthly,
      required this.date,
      required this.displayAverage});

  @override
  State<StatefulWidget> createState() => BarChartSample5State();
}

class BarChartSample5State extends State<BarChartSample5> {
  static const double barWidth = 22;
  static const shadowOpacity = 0.2;
  int touchedIndex = -1;

  Map<int, double> expendituresAcrossTheYear = <int, double>{
    //first element is the variable income, second is the fixed income, third is the variable expense, fourth is the fixed expense
  };

  @override
  void initState() {
    super.initState();
  }

  Widget bottomTitles(double value, TitleMeta meta) {
    const style = TextStyle(color: Colors.white, fontSize: 10);
    String text;
    switch (value.toInt()) {
      case 0:
        text = 'Mon';
        break;
      case 1:
        text = 'Tue';
        break;
      case 2:
        text = 'Wed';
        break;
      case 3:
        text = 'Thu';
        break;
      case 4:
        text = 'Fri';
        break;
      case 5:
        text = 'Sat';
        break;
      case 6:
        text = 'Sun';
        break;
      default:
        text = '';
        break;
    }
    return SideTitleWidget(
      axisSide: meta.axisSide,
      child: Text(text, style: style),
    );
  }

  Widget topTitles(double value, TitleMeta meta) {
    const style = TextStyle(color: Colors.white, fontSize: 10);
    String text;
    switch (value.toInt()) {
      case 0:
        text = 'Mon';
        break;
      case 1:
        text = 'Tue';
        break;
      case 2:
        text = 'Wed';
        break;
      case 3:
        text = 'Thu';
        break;
      case 4:
        text = 'Fri';
        break;
      case 5:
        text = 'Sat';
        break;
      case 6:
        text = 'Sun';
        break;
      default:
        return Container();
    }
    return SideTitleWidget(
      axisSide: meta.axisSide,
      child: Text(text, style: style),
    );
  }

  Widget leftTitles(double value, TitleMeta meta) {
    const style = TextStyle(color: Colors.white, fontSize: 10);
    String text;
    if (value == 0) {
      text = '0';
    } else {
      text = '${value.toInt()}0k';
    }
    return SideTitleWidget(
      axisSide: meta.axisSide,
      space: 4,
      child: Text(
        text,
        style: style,
        textAlign: TextAlign.center,
      ),
    );
  }

  Widget rightTitles(double value, TitleMeta meta) {
    const style = TextStyle(color: Colors.white, fontSize: 10);
    String text;
    if (value == 0) {
      text = '0';
    } else {
      text = '${value.toInt()}0k';
    }
    return SideTitleWidget(
      axisSide: meta.axisSide,
      space: 0,
      child: Text(
        text,
        style: style,
        textAlign: TextAlign.center,
      ),
    );
  }

  BarChartGroupData generateGroup(int x, double value1) {
    final isTop = value1 > 0;
    final sum = value1;
    final isTouched = touchedIndex == x;
    return BarChartGroupData(
      x: x,
      groupVertically: true,
      showingTooltipIndicators: isTouched ? [0] : [],
      barRods: [
        BarChartRodData(
          toY: sum,
          width: barWidth,
          borderRadius: isTop
              ? const BorderRadius.only(
                  topLeft: Radius.circular(6),
                  topRight: Radius.circular(6),
                )
              : const BorderRadius.only(
                  bottomLeft: Radius.circular(6),
                  bottomRight: Radius.circular(6),
                ),
          rodStackItems: [
            BarChartRodStackItem(
              0,
              value1,
              Colors.green,
              BorderSide(
                color: Colors.white,
                width: isTouched ? 2 : 0,
              ),
            ),
          ],
        ),
        BarChartRodData(
          toY: -sum,
          width: barWidth,
          color: Colors.transparent,
          borderRadius: isTop
              ? const BorderRadius.only(
                  bottomLeft: Radius.circular(6),
                  bottomRight: Radius.circular(6),
                )
              : const BorderRadius.only(
                  topLeft: Radius.circular(6),
                  topRight: Radius.circular(6),
                ),
          rodStackItems: [],
        ),
      ],
    );
  }

  bool isShadowBar(int rodIndex) => rodIndex == 1;

  @override
  Widget build(BuildContext context) {
    populateBarChartData();
    return AspectRatio(
      aspectRatio: 0.8,
      child: Padding(
        padding: const EdgeInsets.only(top: 16),
        child: BarChart(
          BarChartData(
            alignment: BarChartAlignment.spaceEvenly,
            maxY: 20,
            minY: -20,
            groupsSpace: 12,
            barTouchData: BarTouchData(
              handleBuiltInTouches: false,
              touchCallback: (FlTouchEvent event, barTouchResponse) {
                if (!event.isInterestedForInteractions ||
                    barTouchResponse == null ||
                    barTouchResponse.spot == null) {
                  setState(() {
                    touchedIndex = -1;
                  });
                  return;
                }
                final rodIndex = barTouchResponse.spot!.touchedRodDataIndex;
                if (isShadowBar(rodIndex)) {
                  setState(() {
                    touchedIndex = -1;
                  });
                  return;
                }
                setState(() {
                  touchedIndex = barTouchResponse.spot!.touchedBarGroupIndex;
                });
              },
            ),
            titlesData: FlTitlesData(
              show: true,
              topTitles: AxisTitles(
                sideTitles: SideTitles(
                  showTitles: true,
                  reservedSize: 32,
                  getTitlesWidget: topTitles,
                ),
              ),
              bottomTitles: AxisTitles(
                sideTitles: SideTitles(
                  showTitles: true,
                  reservedSize: 32,
                  getTitlesWidget: bottomTitles,
                ),
              ),
              leftTitles: AxisTitles(
                sideTitles: SideTitles(
                  showTitles: true,
                  getTitlesWidget: leftTitles,
                  interval: 5,
                  reservedSize: 42,
                ),
              ),
              rightTitles: AxisTitles(
                sideTitles: SideTitles(
                  showTitles: true,
                  getTitlesWidget: rightTitles,
                  interval: 5,
                  reservedSize: 42,
                ),
              ),
            ),
            gridData: FlGridData(
              show: true,
              checkToShowHorizontalLine: (value) => value % 5 == 0,
              getDrawingHorizontalLine: (value) {
                if (value == 0) {
                  return FlLine(
                    strokeWidth: 3,
                  );
                }
                return FlLine(
                  strokeWidth: 0.8,
                );
              },
            ),
            borderData: FlBorderData(
              show: false,
            ),
            barGroups: expendituresAcrossTheYear.entries
                .map(
                  (e) => generateGroup(e.key, e.value),
                )
                .toList(),
          ),
        ),
      ),
    );
  }

  void populateBarChartData() {
    getTrueValueOfExpenditures(
        DateTime(widget.date.year, 1, 1),
        DateTime(widget.date.year, 12, 31),
        widget.expenditures); //This will be for yearly data
  }

  void getTrueValueOfExpenditures(
      DateTime from, DateTime to, List<Expenditure> expenditures) {
    expendituresAcrossTheYear.clear();
    //... performs calculations and directly updates the expendituresAcrossTheYear map vaiable (not using setState();)
  }
}

解决方案

1. 修复柱状图溢出问题

  • 匹配数据范围与maxY/minY:如果实际数据值超过设置的maxY:20或minY:-20,图表会溢出容器。根据expendituresAcrossTheYear中的最大/最小值,动态调整maxY和minY(比如设为数据最大值的1.1倍),确保有足够显示空间。
  • 调整AspectRatio:当前aspectRatio:0.8会让图表高度大于宽度,容易撑满窗口。改为示例中的1.7或根据布局需求调整为合适的值(比如1.5)。
  • 正确处理数据更新:不要在build方法中调用populateBarChartData,而是在initState和didUpdateWidget中调用,并且更新数据后必须调用setState()触发UI刷新:
    @override
    void initState() {
      super.initState();
      populateBarChartData();
    }
    
    @override
    void didUpdateWidget(covariant BarChartSample5 oldWidget) {
      super.didUpdateWidget(oldWidget);
      if (oldWidget.expenditures != widget.expenditures || oldWidget.date != widget.date) {
        populateBarChartData();
      }
    }
    
    void getTrueValueOfExpenditures(DateTime from, DateTime to, List<Expenditure> expenditures) {
      expendituresAcrossTheYear.clear();
      // ... 你的计算逻辑
      setState(() {}); // 更新数据后触发UI刷新
    }
    

2. 修复轴标签不显示问题

  • 修正文本颜色与背景的对比度:代码中所有轴标签的文本颜色设为Colors.white,如果图表所在容器背景是浅色(比如默认白色),标签会完全看不见。将文本颜色改为深色(比如Colors.black87):
    const style = TextStyle(color: Colors.black87, fontSize: 10);
    
  • 匹配x轴分组的key值:当前bottomTitles和topTitles的switch分支只处理0-6的x值,但代码是按年份生成月份数据(x值为1-12),导致匹配不到任何分支,显示空文本。修改为适配月份的逻辑:
    Widget bottomTitles(double value, TitleMeta meta) {
      const style = TextStyle(color: Colors.black87, fontSize: 10);
      final monthIndex = value.toInt();
      final months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
      String text = monthIndex >=1 && monthIndex <=12 ? months[monthIndex-1] : '';
      return SideTitleWidget(
        axisSide: meta.axisSide,
        child: Text(text, style: style),
      );
    }
    
  • 检查SideTitles配置:确保reservedSize足够容纳标签文本,避免被截断。如果文本较长,适当增大reservedSize的值。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 07:20:56