Vertical dotted line on chart

2.6k views Asked by At

I actually use two dependancy: syncfusion flutter chart & dotted line

My chart look like this (see picture 1)

enter image description here

You can tap on the bullet point on the chart (or the basketball emoji) to display a dotted line and a tooltip widget. My problem is that if there is not enough height on my chart, the displayed widget go on the bottom (which is not our goal, see picture 2). How can i achieve to display my widget always on top of the chart ?

enter image description here

I searched with overflow boxes and stacks, but after some long hours, i could make it by myself. The code for the chart is available here:

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,
                ),
              ),
            )
        ],
      ),
    );
  }
}
1

There are 1 answers

0
srk_sync On BEST ANSWER

Currently, when there is no space above the data point to render its custom tooltip widget, it gets shift downwards and gets rendered. This is the default behaviour. However, if you want to always render the custom tooltip widget above the respective data point, then you can set rangePadding property of the chart axis to ChartRangePadding.additional so that there additional range padding will be added at the start and end of the axis. Also, if providing rangePadding value as additional didn’t solve the issue then you need to set the specific range for the chart according to you data points available by using the maximum property of the chart axis so that there is enough space for the tooltip widget builder to get render above the respective data point. Please refer the code snippets below for further reference.

Code snippet:

Setting rangePadding property value as additional:

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

Setting specific range for the chart’s y-axis:

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

For further reference on the rangePadding property and customizing the range of the chart, please check the user guides below.