diff --git a/example/lib/line_chart/line_chart_page3.dart b/example/lib/line_chart/line_chart_page3.dart index d3c2aaacc..5f050bda6 100644 --- a/example/lib/line_chart/line_chart_page3.dart +++ b/example/lib/line_chart/line_chart_page3.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'samples/line_chart_sample6.dart'; +import 'samples/line_chart_sample9.dart'; class LineChartPage3 extends StatelessWidget { @override @@ -20,6 +21,17 @@ class LineChartPage3 extends StatelessWidget { height: 52, ), LineChartSample6(), + const SizedBox( + height: 52, + ), + const Text( + 'LineChart (positive and negative values)', + style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: Colors.black), + ), + const SizedBox( + height: 52, + ), + LineChartSample9(), ], ), ), diff --git a/example/lib/line_chart/samples/line_chart_sample8.dart b/example/lib/line_chart/samples/line_chart_sample8.dart index 96d192301..2a24036a5 100644 --- a/example/lib/line_chart/samples/line_chart_sample8.dart +++ b/example/lib/line_chart/samples/line_chart_sample8.dart @@ -151,7 +151,7 @@ class _LineChartSample8State extends State { ), ), lineTouchData: LineTouchData( - fullHeightTouchLine: true, + getTouchLineEnd: (data, index) => double.infinity, getTouchedSpotIndicator: (LineChartBarData barData, List spotIndexes) { return spotIndexes.map((spotIndex) { return TouchedSpotIndicatorData( diff --git a/example/lib/line_chart/samples/line_chart_sample9.dart b/example/lib/line_chart/samples/line_chart_sample9.dart new file mode 100644 index 000000000..96e2e38b8 --- /dev/null +++ b/example/lib/line_chart/samples/line_chart_sample9.dart @@ -0,0 +1,92 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'dart:math'; + +// ignore: must_be_immutable +class LineChartSample9 extends StatelessWidget { + final spots = List.generate(101, (i) => (i - 50) / 10).map((x) => FlSpot(x, sin(x))).toList(); + + LineChartSample9() {} + + @override + Widget build(BuildContext context) { + return Container( + child: Padding( + padding: const EdgeInsets.only(right: 22.0, bottom: 20), + child: SizedBox( + width: 400, + height: 400, + child: LineChart( + LineChartData( + lineTouchData: LineTouchData( + touchTooltipData: LineTouchTooltipData( + maxContentWidth: 100, + tooltipBgColor: Colors.orange, + getTooltipItems: (touchedSpots) { + return touchedSpots.map((LineBarSpot touchedSpot) { + final textStyle = TextStyle( + color: touchedSpot.bar.colors[0], + fontWeight: FontWeight.bold, + fontSize: 14, + ); + return LineTooltipItem( + '${touchedSpot.x}, ${touchedSpot.y.toStringAsFixed(2)}', textStyle); + }).toList(); + }), + handleBuiltInTouches: true, + getTouchLineStart: (data, index) => 0, + ), + lineBarsData: [ + LineChartBarData( + colors: [ + Colors.black, + ], + spots: spots, + isCurved: true, + isStrokeCapRound: true, + barWidth: 3, + belowBarData: BarAreaData( + show: false, + ), + dotData: FlDotData(show: false), + ), + ], + minY: -1.5, + maxY: 1.5, + titlesData: FlTitlesData( + leftTitles: SideTitles( + showTitles: true, + getTextStyles: (value) => const TextStyle( + color: Colors.blueGrey, fontWeight: FontWeight.bold, fontSize: 18), + margin: 16, + ), + rightTitles: SideTitles(showTitles: false), + bottomTitles: SideTitles( + showTitles: true, + getTextStyles: (value) => const TextStyle( + color: Colors.blueGrey, fontWeight: FontWeight.bold, fontSize: 18), + margin: 16, + ), + topTitles: SideTitles(showTitles: false), + ), + gridData: FlGridData( + show: true, + drawHorizontalLine: true, + drawVerticalLine: true, + horizontalInterval: 1.5, + verticalInterval: 5, + checkToShowHorizontalLine: (value) { + return value.toInt() == 0; + }, + checkToShowVerticalLine: (value) { + return value.toInt() == 0; + }, + ), + borderData: FlBorderData(show: false), + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/chart/line_chart/line_chart_data.dart b/lib/src/chart/line_chart/line_chart_data.dart index c95f440b5..bf5e79fdc 100644 --- a/lib/src/chart/line_chart/line_chart_data.dart +++ b/lib/src/chart/line_chart/line_chart_data.dart @@ -780,10 +780,10 @@ class FlDotData with EquatableMixin { /// This class contains the interface that all DotPainters should conform to. abstract class FlDotPainter with EquatableMixin { - /// This method should be overriden to draw the dot shape. + /// This method should be overridden to draw the dot shape. void draw(Canvas canvas, FlSpot spot, Offset offsetInCanvas); - /// This method should be overriden to return the size of the shape. + /// This method should be overridden to return the size of the shape. Size getSize(FlSpot spot); } @@ -1303,9 +1303,13 @@ class LineTouchData extends FlTouchData with EquatableMixin { /// [LineTouchResponse] shows a tooltip popup above the touched spot. final bool handleBuiltInTouches; - /// Sets the indicator line full height, from bottom to top of the chart, - /// and goes through the targeted spot. - final bool fullHeightTouchLine; + /// The starting point on y axis of the touch line. By default, line starts on the bottom of + /// the chart. + final GetTouchLineY getTouchLineStart; + + /// The end point on y axis of the touch line. By default, line ends at the touched point. + /// If line end is overlap with the dot, it will be automatically adjusted to the edge of the dot. + final GetTouchLineY getTouchLineEnd; /// Informs the touchResponses final LineTouchCallback? touchCallback; @@ -1315,10 +1319,8 @@ class LineTouchData extends FlTouchData with EquatableMixin { /// touch occurs (or you can show it manually using, [LineChartData.showingTooltipIndicators]) /// and also it shows an indicator (contains a thicker line and larger dot on the targeted spot), /// You can define how this indicator looks like through [getTouchedSpotIndicator] callback, - /// You can customize this tooltip using [touchTooltipData], indicator lines starts from bottom - /// of the chart to the targeted spot, you can change this behavior by [fullHeightTouchLine], - /// if [fullHeightTouchLine] sets true, the line goes from bottom to top of the chart, - /// and goes through the targeted spot. + /// You can customize this tooltip using [touchTooltipData], indicator lines starts from position + /// controlled by [getTouchLineStart] and ends at position controlled by [getTouchLineEnd]. /// If you need to have a distance threshold for handling touches, use [touchSpotThreshold]. /// /// You can listen to touch events using [touchCallback], @@ -1329,14 +1331,16 @@ class LineTouchData extends FlTouchData with EquatableMixin { LineTouchTooltipData? touchTooltipData, GetTouchedSpotIndicator? getTouchedSpotIndicator, double? touchSpotThreshold, - bool? fullHeightTouchLine, bool? handleBuiltInTouches, + GetTouchLineY? getTouchLineStart, + GetTouchLineY? getTouchLineEnd, LineTouchCallback? touchCallback, }) : touchTooltipData = touchTooltipData ?? LineTouchTooltipData(), getTouchedSpotIndicator = getTouchedSpotIndicator ?? defaultTouchedIndicators, touchSpotThreshold = touchSpotThreshold ?? 10, - fullHeightTouchLine = fullHeightTouchLine ?? false, handleBuiltInTouches = handleBuiltInTouches ?? true, + getTouchLineStart = getTouchLineStart ?? defaultGetTouchLineStart, + getTouchLineEnd = getTouchLineEnd ?? defaultGetTouchLineEnd, touchCallback = touchCallback, super(enabled ?? true); @@ -1347,7 +1351,8 @@ class LineTouchData extends FlTouchData with EquatableMixin { LineTouchTooltipData? touchTooltipData, GetTouchedSpotIndicator? getTouchedSpotIndicator, double? touchSpotThreshold, - bool? fullHeightTouchLine, + GetTouchLineY? getTouchLineStart, + GetTouchLineY? getTouchLineEnd, bool? handleBuiltInTouches, Function(LineTouchResponse)? touchCallback, }) { @@ -1356,7 +1361,8 @@ class LineTouchData extends FlTouchData with EquatableMixin { touchTooltipData: touchTooltipData ?? this.touchTooltipData, getTouchedSpotIndicator: getTouchedSpotIndicator ?? this.getTouchedSpotIndicator, touchSpotThreshold: touchSpotThreshold ?? this.touchSpotThreshold, - fullHeightTouchLine: fullHeightTouchLine ?? this.fullHeightTouchLine, + getTouchLineStart: getTouchLineStart ?? this.getTouchLineStart, + getTouchLineEnd: getTouchLineEnd ?? this.getTouchLineEnd, handleBuiltInTouches: handleBuiltInTouches ?? this.handleBuiltInTouches, touchCallback: touchCallback ?? this.touchCallback, ); @@ -1369,7 +1375,8 @@ class LineTouchData extends FlTouchData with EquatableMixin { getTouchedSpotIndicator, touchSpotThreshold, handleBuiltInTouches, - fullHeightTouchLine, + getTouchLineStart, + getTouchLineEnd, touchCallback, enabled, ]; @@ -1384,6 +1391,9 @@ class LineTouchData extends FlTouchData with EquatableMixin { typedef GetTouchedSpotIndicator = List Function( LineChartBarData barData, List spotIndexes); +/// Used for determine the touch indicator line's starting/end point. +typedef GetTouchLineY = double Function(LineChartBarData barData, int spotIndex); + /// Default presentation of touched indicators. List defaultTouchedIndicators( LineChartBarData barData, List indicators) { @@ -1409,6 +1419,16 @@ List defaultTouchedIndicators( }).toList(); } +/// By default line starts from the bottom of the chart. +double defaultGetTouchLineStart(LineChartBarData barData, int spotIndex) { + return -double.infinity; +} + +/// By default line ends at the touched point. +double defaultGetTouchLineEnd(LineChartBarData barData, int spotIndex) { + return barData.spots[spotIndex].y; +} + /// Holds representation data for showing tooltip popup on top of spots. class LineTouchTooltipData with EquatableMixin { /// The tooltip background color. diff --git a/lib/src/chart/line_chart/line_chart_painter.dart b/lib/src/chart/line_chart/line_chart_painter.dart index ee44c5ba5..dc9aabd59 100644 --- a/lib/src/chart/line_chart/line_chart_painter.dart +++ b/lib/src/chart/line_chart/line_chart_painter.dart @@ -1,3 +1,4 @@ +import 'dart:math'; import 'dart:ui' as ui; import 'dart:ui'; @@ -313,19 +314,30 @@ class LineChartPainter extends AxisChartPainter { } /// For drawing the indicator line - final bottom = Offset(touchedSpot.dx, getTopOffsetDrawSize(holder) + chartViewSize.height); - final top = Offset(getPixelX(spot.x, chartViewSize, holder), getTopOffsetDrawSize(holder)); - - /// Draw to top or to the touchedSpot - final lineEnd = - data.lineTouchData.fullHeightTouchLine ? top : touchedSpot + Offset(0, dotHeight / 2); + final lineStartY = + min(data.maxY, max(data.minY, data.lineTouchData.getTouchLineStart(barData, index))); + final lineEndY = + min(data.maxY, max(data.minY, data.lineTouchData.getTouchLineEnd(barData, index))); + final lineStart = Offset(touchedSpot.dx, getPixelY(lineStartY, chartViewSize, holder)); + var lineEnd = Offset(touchedSpot.dx, getPixelY(lineEndY, chartViewSize, holder)); + + /// If line end is inside the dot, adjust it so that it doesn't overlap with the dot. + final dotMinY = touchedSpot.dy - dotHeight / 2; + final dotMaxY = touchedSpot.dy + dotHeight / 2; + if (lineEnd.dy > dotMinY && lineEnd.dy < dotMaxY) { + if (lineStart.dy < lineEnd.dy) { + lineEnd -= Offset(0, lineEnd.dy - dotMinY); + } else { + lineEnd += Offset(0, dotMaxY - lineEnd.dy); + } + } _touchLinePaint.color = indicatorData.indicatorBelowLine.color; _touchLinePaint.strokeWidth = indicatorData.indicatorBelowLine.strokeWidth; _touchLinePaint.transparentIfWidthIsZero(); canvasWrapper.drawDashedLine( - bottom, lineEnd, _touchLinePaint, indicatorData.indicatorBelowLine.dashArray); + lineStart, lineEnd, _touchLinePaint, indicatorData.indicatorBelowLine.dashArray); /// Draw the indicator dot if (showingDots) { diff --git a/repo_files/documentations/line_chart.md b/repo_files/documentations/line_chart.md index 7e0399be0..d793e9220 100644 --- a/repo_files/documentations/line_chart.md +++ b/repo_files/documentations/line_chart.md @@ -169,7 +169,8 @@ When you change the chart's state, it animates to the new state internally (usin |getTouchedSpotIndicator| a callback that retrieves list of [TouchedSpotIndicatorData](#TouchedSpotIndicatorData) by the given list of [LineBarSpot](#LineBarSpot) for showing the indicators on touched spots|defaultTouchedIndicators| |touchSpotThreshold|the threshold of the touch accuracy|10| |handleBuiltInTouches| set this true if you want the built in touch handling (show a tooltip bubble and an indicator on touched spots) | true| -|fullHeightTouchLine| set `true` to show the line in full height mode | false| +|getTouchLineStart| controls where the line starts, default is bottom of the chart| defaultGetTouchLineStart| +|getTouchLineEnd| controls where the line ends, default is the touch point| defaultGetTouchLineEnd| |touchCallback| listen to this callback to retrieve touch events, it gives you a [LineTouchResponse](#LineTouchResponse)| null| diff --git a/test/chart/data_pool.dart b/test/chart/data_pool.dart index 1af1d4ec7..b9c5a5f39 100644 --- a/test/chart/data_pool.dart +++ b/test/chart/data_pool.dart @@ -990,7 +990,6 @@ final LineTouchData lineTouchData1 = LineTouchData( handleBuiltInTouches: false, touchSpotThreshold: 12, touchTooltipData: lineTouchTooltipData1, - fullHeightTouchLine: false, ); final LineTouchData lineTouchData1Clone = LineTouchData( enabled: true, @@ -999,7 +998,6 @@ final LineTouchData lineTouchData1Clone = LineTouchData( handleBuiltInTouches: false, touchSpotThreshold: 12, touchTooltipData: lineTouchTooltipData1, - fullHeightTouchLine: false, ); final LineTouchData lineTouchData2 = LineTouchData( @@ -1009,7 +1007,6 @@ final LineTouchData lineTouchData2 = LineTouchData( handleBuiltInTouches: false, touchSpotThreshold: 12, touchTooltipData: lineTouchTooltipData1, - fullHeightTouchLine: false, ); final LineTouchData lineTouchData3 = LineTouchData( enabled: true, @@ -1018,7 +1015,6 @@ final LineTouchData lineTouchData3 = LineTouchData( handleBuiltInTouches: false, touchSpotThreshold: 12, touchTooltipData: lineTouchTooltipData1, - fullHeightTouchLine: false, ); final LineTouchData lineTouchData4 = LineTouchData( enabled: true, @@ -1027,7 +1023,6 @@ final LineTouchData lineTouchData4 = LineTouchData( handleBuiltInTouches: false, touchSpotThreshold: 12, touchTooltipData: null, - fullHeightTouchLine: false, ); final LineTouchData lineTouchData5 = LineTouchData( enabled: true, @@ -1036,7 +1031,6 @@ final LineTouchData lineTouchData5 = LineTouchData( handleBuiltInTouches: false, touchSpotThreshold: 12.001, touchTooltipData: lineTouchTooltipData1, - fullHeightTouchLine: false, ); final LineTouchData lineTouchData6 = LineTouchData( enabled: true, @@ -1045,7 +1039,6 @@ final LineTouchData lineTouchData6 = LineTouchData( handleBuiltInTouches: true, touchSpotThreshold: 12, touchTooltipData: lineTouchTooltipData1, - fullHeightTouchLine: false, ); final LineTouchData lineTouchData7 = LineTouchData( enabled: true, @@ -1054,7 +1047,7 @@ final LineTouchData lineTouchData7 = LineTouchData( handleBuiltInTouches: false, touchSpotThreshold: 12, touchTooltipData: lineTouchTooltipData1, - fullHeightTouchLine: true, + getTouchLineEnd: (barData, index) => double.infinity, ); final String Function(HorizontalLine) horizontalLabelResolver = (horizontalLine) => 'test';