图表上的垂直虚线

Vertical dotted line on chart

我其实用了两个依赖:syncfusion flutter chart & dotted line

我的图表是这样的(见图 1)

您可以点击图表上的项目符号点(或篮球表情符号)以显示虚线和工具提示小部件。 我的问题是,如果我的图表上没有足够的高度,则显示的小部件会位于底部(这不是我们的目标,请参见图 2)。 我怎样才能使我的小部件始终显示在图表顶部?

我用溢出框和堆栈进行了搜索,但经过几个小时后,我可以自己完成。 图表代码可在此处获得:

https://pastebin.com/6VX1VhRH

import 'package:dotted_line/dotted_line.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:lifetizr/misc/utils.dart';
import 'package:lifetizr/ui/screens/homepage/homepage_viewmodel.dart';
import 'package:lifetizr/ui/widgets/activity_block_preview.dart';
import 'package:stacked/stacked.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
import 'package:theme_provider/theme_provider.dart';

class ChartBlock extends StatefulWidget {
  @override
  _ChartBlockState createState() => _ChartBlockState();
}

class _ChartBlockState extends State<ChartBlock> {
  List<_DataPoint> graphData = [];

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

  void loadData() async {
    await HomepageViewModel().getGraphData().then(
      (value) {
        List<_DataPoint> tmpData = [];

        tmpData.add(_DataPoint(DateTime.fromMillisecondsSinceEpoch(0).toString(), 290));

        for (var item in value['allBloodglucoses']['edges']) {
          tmpData.add(_DataPoint(
              item['node']['timestamp'], item['node']['bloodGlucose']));
        }

        setState(
          () {
            graphData = tmpData;
          },
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    final controller = ThemeProvider.controllerOf(context);

    return ViewModelBuilder<HomepageViewModel>.reactive(
      viewModelBuilder: () => HomepageViewModel(),
      builder: (context, model, child) {
        return SfCartesianChart(
          trackballBehavior: TrackballBehavior(
              lineWidth: 5,
              lineColor: reverseColorGrabber(controller),
              lineType: TrackballLineType.vertical,
              enable: true,
              shouldAlwaysShow: true,
              tooltipAlignment: ChartAlignment.far,
              tooltipSettings: InteractiveTooltip(
                arrowWidth: 0,
                arrowLength: 0,
                borderColor: reverseColorGrabber(controller),
                borderRadius: 15,
                borderWidth: 15,
                connectorLineWidth: 50,
                format: 'point.y mg/dL',
                enable: true,
                color: reverseColorGrabber(controller),
              )),
          primaryXAxis: CategoryAxis(
              visibleMinimum: 0,
              visibleMaximum: graphData.length.toDouble() / 6),
          zoomPanBehavior: ZoomPanBehavior(
            enablePanning: true,
          ),
          indicators: [],
          // Enable tooltip
          tooltipBehavior: TooltipBehavior(
            color: Colors.transparent,
            canShowMarker: false,
            enable: true,
            builder: (dynamic d, dynamic f, dynamic g, int i, int j) {
              if (i == 1 || i == 4 || i == 3)
                return Container(
                  height: 100,
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.end,
                    children: [
                      Container(
                        padding: EdgeInsets.all(5),
                        decoration: BoxDecoration(
                          borderRadius: BorderRadius.circular(25),
                          color: Colors.green,
                        ),
                        child: InkWell(
                          onTap: () {
                            print("Pressed");
                          },
                          child: Container(
                              width: 80,
                              child: ActivityBlockPreview(
                                score: 18,
                                pictureUrl: "https://i.imgur.com/nlRr5vn.jpeg",
                              )),
                        ),
                      ),
                      Expanded(
                        child: DottedLine(
                          direction: Axis.vertical,
                          lineLength: double.infinity,
                          lineThickness: 1.0,
                          dashLength: 4.0,
                          dashColor: reverseColorGrabber(controller),
                          dashRadius: 0.0,
                          dashGapLength: 4.0,
                          dashGapColor: Colors.transparent,
                          dashGapRadius: 0.0,
                        ),
                      ),
                    ],
                  ),
                );
              else
                return null;
            },
          ),
          series: <ChartSeries<_DataPoint, String>>[
            SplineSeries<_DataPoint, String>(
              color: reverseColorGrabber(controller),
              width: 5,
              pointColorMapper: (_DataPoint data, int i) {
                if (i == 0) return Colors.transparent;
              },
              dataSource: <_DataPoint>[...graphData],
              xValueMapper: (_DataPoint point, _) =>
                  DateFormat('H:mm').format(DateTime.parse(point.timestamp)),
              yValueMapper: (_DataPoint point, _) => point.glucose,
              // Enable data label
              dataLabelSettings: DataLabelSettings(
                isVisible: true,
                labelAlignment: ChartDataLabelAlignment.top,
                builder: (dynamic d, dynamic f, dynamic g, int i, int _) {
                  if (i == 1 || i == 4)
                    return Container(
                      height: 20,
                      width: 20,
                      decoration: BoxDecoration(
                        borderRadius: BorderRadius.circular(30),
                        border: Border.all(width: 3, color: Colors.red),
                        color: Colors.white,
                      ),
                    );
                  if (i == 3) {
                    return Text(
                      "",
                      style: TextStyle(fontSize: 20),
                    );
                  } else
                    return null;
                },
              ),
            )
          ],
        );
      },
    );
  }
}

class _DataPoint {
  _DataPoint(this.timestamp, this.glucose);

  final String timestamp;
  final double glucose;
}


class ActivityBlockPreview extends StatefulWidget {
  final int score;
  final String pictureUrl;
  final bool isSportActivity;
  final bool isSleepActivity;
  final String sportEmoji;

  const ActivityBlockPreview(
      {this.score,
      this.pictureUrl,
      this.isSportActivity = false,
      this.isSleepActivity = false,
      this.sportEmoji});

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

class _ActivityBlockPreviewState extends State<ActivityBlockPreview> {
  @override
  Widget build(BuildContext context) {
    var controller = ThemeProvider.controllerOf(context);

    return Container(
      decoration: BoxDecoration(
          color: colorGrabber(controller),
          borderRadius: BorderRadius.circular(60)),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.start,
        children: [
          CircleAvatar(
            backgroundColor: colorGrabber(controller),
            backgroundImage: widget.isSportActivity == false &&
                    widget.isSleepActivity == false
                ? NetworkImage(
                    widget.pictureUrl,
                  )
                : null,
            child: widget.isSportActivity == true || widget.isSleepActivity
                ? Text(widget.sportEmoji)
                : Container(),
          ),
          if (!widget.isSportActivity)
            Padding(
              padding: const EdgeInsets.only(left: 5, right: 10),
              child: Text(
                widget.score.toString(),
                style: TextStyle(
                  fontSize: 20,
                  fontWeight: FontWeight.bold,
                ),
              ),
            )
        ],
      ),
    );
  }
}

目前,当数据点上方没有 space 来呈现其自定义工具提示小部件时,它会向下移动并呈现。这是默认行为。但是,如果您希望始终在相应数据点上方呈现自定义工具提示小部件,则可以将图表轴的 rangePadding 属性 设置为 ChartRangePadding.additional 以便在轴的起点和终点添加额外的范围填充。此外,如果提供 rangePadding 值作为 additional 没有解决问题,那么您需要设置根据您使用图表轴的最大值 属性 可用的数据点,图表的特定范围,以便工具提示小部件构建器有足够的 space 来在相应数据点上方呈现。请参考下面的代码片段以供进一步参考。

代码片段:

将 rangePadding 属性 值设置为附加值:

SfCartesianChart(
          primaryYAxis: NumericAxis(
              rangePadding: ChartRangePadding.additional
          ),
          // Your configurations
)

为图表的 y 轴设置特定范围:

SfCartesianChart(
          primaryYAxis: NumericAxis(
              minimum: 0,
              maximum: 300
          ),
          // Your configurations
)

有关 rangePadding 属性 和自定义图表范围的更多参考,请查看下面的用户指南。