diff --git a/example/lib/line_chart/line_chart_page.dart b/example/lib/line_chart/line_chart_page.dart index 946b591c0..0024118dd 100644 --- a/example/lib/line_chart/line_chart_page.dart +++ b/example/lib/line_chart/line_chart_page.dart @@ -1,3 +1,5 @@ +import 'package:example/line_chart/samples/line_chart_sample6.dart'; + import 'samples/line_chart_sample1.dart'; import 'samples/line_chart_sample2.dart'; import 'package:flutter/material.dart'; @@ -39,6 +41,16 @@ class LineChartPage extends StatelessWidget { padding: const EdgeInsets.only(left: 28.0, right: 28), child: LineChartSample2(), ), + SizedBox( + height: 22, + ), + Padding( + padding: const EdgeInsets.only(left: 28.0, right: 28), + child: LineChartSample6(), + ), + SizedBox( + height: 22, + ), ], ), ); diff --git a/example/lib/line_chart/samples/line_chart_sample6.dart b/example/lib/line_chart/samples/line_chart_sample6.dart new file mode 100644 index 000000000..41facafc2 --- /dev/null +++ b/example/lib/line_chart/samples/line_chart_sample6.dart @@ -0,0 +1,126 @@ +import 'dart:math'; + +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; + +class LineChartSample6 extends StatelessWidget { + static const double _minX = 0; + static const double _maxX = 5; + static const double _minY = 100; + static const double _maxY = 500; + + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 1.23, + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(6)), + color: Colors.white + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox( + height: 27, + ), + Text('Monthly spending', style: TextStyle(color: Colors.black54, fontSize: 16,), textAlign: TextAlign.center,), + const SizedBox( + height: 4, + ), + Text('Cafés, Restaurants and Fast Food', style: TextStyle(color: Colors.black45, fontSize: 12, letterSpacing: 2), textAlign: TextAlign.center,), + const SizedBox( + height: 17, + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only(right: 25.0, left: 25.0), + child: FlChart( + chart: LineChart( + LineChartData( + gridData: const FlGridData( + show: false, + ), + extraLinesData: const ExtraLinesData( + show: true, + showAverageLine: true, + averageLineStyle: LineStyle( + color: Colors.black26, + dashed: true, + dashDefinition: DashDefinition(solidWidth: 2, gapWidth: 2) + ), + showDataPointLines: true, + dataPointLineStyle: LineStyle( + color: Colors.black12 + ) + ), + titlesData: FlTitlesData( + horizontalTitlesTextStyle: TextStyle( + color: Colors.black54, + fontSize: 12, + ), + getHorizontalTitles: (index) { + switch (index.toInt()) { + case 0: + return 'Jan'; + case 1: + return 'Feb'; + case 2: + return 'Mar'; + case 3: + return 'Apr'; + case 4: + return 'May'; + case 5: + return 'Jun'; + default: + return ''; + } + }, + showVerticalTitles: false, + ), + borderData: FlBorderData( + show: false, + ), + minX: _minX, + maxX: _maxX, + minY: _minY, + maxY: _maxY, + lineBarsData: [ + LineChartBarData( + spots: _getSeriesSpots(), + isCurved: true, + colors: [ + const Color(0xff3badc4), + ], + barWidth: 2, + isStrokeCapRound: false, + dotData: const FlDotData( + show: false, + ), + belowBarData: const BelowBarData( + show: false, + ), + ), + ], + ), + ), + ), + ), + ), + const SizedBox(height: 10,), + ], + ), + ), + ); + } + + List _getSeriesSpots() { + final Random rnd = Random(4); + final List spots = []; + for (double index = 0; index <= _maxX; index++) { + spots.add(FlSpot(index, _minY + (_maxY - _minY) * rnd.nextDouble())); + } + return spots; + } +} \ No newline at end of file diff --git a/lib/src/chart/line_chart/line_chart_data.dart b/lib/src/chart/line_chart/line_chart_data.dart index d30e9254e..f60a36cb5 100644 --- a/lib/src/chart/line_chart/line_chart_data.dart +++ b/lib/src/chart/line_chart/line_chart_data.dart @@ -10,10 +10,12 @@ import 'package:flutter/material.dart'; class LineChartData extends AxisChartData { final List lineBarsData; final FlTitlesData titlesData; + final ExtraLinesData extraLinesData; LineChartData({ this.lineBarsData = const [], this.titlesData = const FlTitlesData(), + this.extraLinesData, FlGridData gridData = const FlGridData(), FlBorderData borderData, double minX, @@ -35,7 +37,7 @@ class LineChartData extends AxisChartData { ) { lineBarsData.forEach((lineBarChart) { if (lineBarChart.spots == null || lineBarChart.spots.isEmpty) { - throw Exception('spots could not be null or empty'); + throw Exception('spots must not be null or empty'); } }); if (lineBarsData.isNotEmpty) { @@ -129,6 +131,9 @@ class LineChartBarData { /// to show dot spots upon the line chart final FlDotData dotData; + // to show line chart annotations such as average line and vertical dot lines + final ExtraLinesData extraLinesData; + const LineChartBarData({ this.spots = const [], this.show = true, @@ -140,6 +145,7 @@ class LineChartBarData { this.isStrokeCapRound = false, this.belowBarData = const BelowBarData(), this.dotData = const FlDotData(), + this.extraLinesData = const ExtraLinesData(), }); } @@ -198,4 +204,52 @@ class FlDotData { this.dotSize = 4.0, this.checkToShowDot = showAllDots, }); +} + +/// This class holds data about drawing chart annotations (data decorations) such as average line and data point lines +class ExtraLinesData { + final bool show; + + // Average line + final bool showAverageLine; + final LineStyle averageLineStyle; + + // Data point lines + final bool showDataPointLines; + final bool terminateAtChartLine; + final LineStyle dataPointLineStyle; + + const ExtraLinesData({ + this.show = true, + + // Average line + this.showAverageLine = true, + this.averageLineStyle = const LineStyle(), + + // Data point lines + this.showDataPointLines = true, + this.terminateAtChartLine = true, + this.dataPointLineStyle = const LineStyle(), + }); +} + +class LineStyle { + final Color color; + final double width; + final bool dashed; + final DashDefinition dashDefinition; + + const LineStyle({ + this.color = Colors.black12, + this.width = 1, + this.dashed = false, + this.dashDefinition = const DashDefinition(solidWidth: 1, gapWidth: 3.5) + }); +} + +class DashDefinition { + final double solidWidth; + final double gapWidth; + + const DashDefinition({this.solidWidth, this.gapWidth}); } \ No newline at end of file diff --git a/lib/src/chart/line_chart/line_chart_painter.dart b/lib/src/chart/line_chart/line_chart_painter.dart index 06b52085c..30d54636c 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 'package:fl_chart/src/chart/base/axis_chart/axis_chart_data.dart'; @@ -13,7 +14,8 @@ class LineChartPainter extends AxisChartPainter { /// [barPaint] is responsible to painting the bar line /// [belowBarPaint] is responsible to fill the below space of our bar line /// [dotPaint] is responsible to draw dots on spot points - Paint barPaint, belowBarPaint, dotPaint; + /// [extraLinesPaint] is responsible for drawing chart annotations, such as average line and dot lines + Paint barPaint, belowBarPaint, dotPaint, extraLinesPaint; LineChartPainter( this.data, @@ -25,6 +27,9 @@ class LineChartPainter extends AxisChartPainter { dotPaint = Paint() ..style = PaintingStyle.fill; + + extraLinesPaint = Paint() + ..style = PaintingStyle.stroke; } @override @@ -40,6 +45,8 @@ class LineChartPainter extends AxisChartPainter { }); drawTitles(canvas, viewSize); + + drawAnnotation(canvas, viewSize); } void drawBarLine(Canvas canvas, Size viewSize, LineChartBarData barData) { @@ -326,4 +333,93 @@ class LineChartPainter extends AxisChartPainter { @override bool shouldRepaint(CustomPainter oldDelegate) => false; + + void drawAnnotation(Canvas canvas, Size viewSize) { + if (data.extraLinesData != null && data.extraLinesData.show) { + if (data.extraLinesData.showAverageLine) { + _drawAverageLine(canvas, viewSize); + } + if (data.extraLinesData.showDataPointLines) { + _drawDataPointLines(canvas, viewSize); + } + return; + } + } + + void _drawDataPointLines(Canvas canvas, Size viewSize) { + viewSize = getChartUsableDrawSize(viewSize); + extraLinesPaint.color = data.extraLinesData.dataPointLineStyle.color; + extraLinesPaint.strokeWidth = data.extraLinesData.dataPointLineStyle.width; + + for (LineChartBarData item in data.lineBarsData) { + for (FlSpot spot in item.spots) { + final double x = getPixelX(spot.x, viewSize); + final double y = getPixelY(spot.y, viewSize); + + if (data.extraLinesData.dataPointLineStyle.dashed) { + _drawDashedLine(canvas, x, viewSize.height, x, y, data.extraLinesData.dataPointLineStyle.dashDefinition); + } else { + canvas.drawLine(Offset(x, viewSize.height), Offset(x, y), extraLinesPaint); + } + } + } + } + + void _drawAverageLine(Canvas canvas, Size viewSize) { + viewSize = getChartUsableDrawSize(viewSize); + extraLinesPaint.color = data.extraLinesData.averageLineStyle.color; + extraLinesPaint.strokeWidth = data.extraLinesData.averageLineStyle.width; + + final double sum = data.lineBarsData + .map((item) => item.spots.map((spot) => spot.y).reduce((value, element) => value + element)) + .reduce((value, element) => value + element); + double numElements = 0; + for (LineChartBarData item in data.lineBarsData) { + numElements += item.spots.length; + } + final double average = sum / numElements; + final double yPos = getPixelY(average, viewSize); + + if (data.extraLinesData.averageLineStyle.dashed) { + _drawDashedLine(canvas, 0, yPos, viewSize.width, yPos, data.extraLinesData.averageLineStyle.dashDefinition); + } else { + final Path averageLinePath = Path(); + averageLinePath.reset(); + averageLinePath.moveTo(0, yPos); + averageLinePath.lineTo(viewSize.width, yPos); + canvas.drawPath(averageLinePath, extraLinesPaint); + } + } + + void _drawDashedLine(Canvas canvas, double startX, double startY, double endX, double endY, + DashDefinition dashDefinition) { + final double originalVectorLength = sqrt(pow(endX - startX, 2) + pow(endY - startY, 2)); + double vectorLength = originalVectorLength; + bool on = true; + + final List normalVector = _normalizeVector(startX, startY, endX, endY); + + while (vectorLength >= 0) { + final double progress = 1 - (vectorLength / originalVectorLength); + final double x1 = startX + (endX - startX) * progress; + final double y1 = startY + (endY - startY) * progress; + if (on) { + final double width = data.extraLinesData.averageLineStyle.dashDefinition.solidWidth; + final double x2 = x1 + (normalVector[0] * width); + final double y2 = y1 + (normalVector[1] * width); + canvas.drawLine(Offset(x1, y1), Offset(x2, y2), extraLinesPaint); + vectorLength -= width; + } else { + vectorLength -= data.extraLinesData.averageLineStyle.dashDefinition.gapWidth; + } + on = !on; + } + } + + List _normalizeVector(double startX, double startY, double endX, double endY) { + final List normal = [endX - startX, endY - startY]; + final double magnitude = sqrt(normal[0] * normal[0] + normal[1] * normal[1]); + final List un = [normal[0] / magnitude, normal[1] / magnitude]; + return un; + } }