Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add average line and data point lines to line charts #12

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions example/lib/line_chart/line_chart_page.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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,
),
],
),
);
Expand Down
126 changes: 126 additions & 0 deletions example/lib/line_chart/samples/line_chart_sample6.dart
Original file line number Diff line number Diff line change
@@ -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: <Widget>[
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<FlSpot> _getSeriesSpots() {
final Random rnd = Random(4);
final List<FlSpot> spots = [];
for (double index = 0; index <= _maxX; index++) {
spots.add(FlSpot(index, _minY + (_maxY - _minY) * rnd.nextDouble()));
}
return spots;
}
}
56 changes: 55 additions & 1 deletion lib/src/chart/line_chart/line_chart_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ import 'package:flutter/material.dart';
class LineChartData extends AxisChartData {
final List<LineChartBarData> 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,
Expand All @@ -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) {
Expand Down Expand Up @@ -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,
Expand All @@ -140,6 +145,7 @@ class LineChartBarData {
this.isStrokeCapRound = false,
this.belowBarData = const BelowBarData(),
this.dotData = const FlDotData(),
this.extraLinesData = const ExtraLinesData(),
});
}

Expand Down Expand Up @@ -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});
}
98 changes: 97 additions & 1 deletion lib/src/chart/line_chart/line_chart_painter.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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,
Expand All @@ -25,6 +27,9 @@ class LineChartPainter extends AxisChartPainter {

dotPaint = Paint()
..style = PaintingStyle.fill;

extraLinesPaint = Paint()
..style = PaintingStyle.stroke;
}

@override
Expand All @@ -40,6 +45,8 @@ class LineChartPainter extends AxisChartPainter {
});

drawTitles(canvas, viewSize);

drawAnnotation(canvas, viewSize);
}

void drawBarLine(Canvas canvas, Size viewSize, LineChartBarData barData) {
Expand Down Expand Up @@ -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<double> 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<double> _normalizeVector(double startX, double startY, double endX, double endY) {
final List<double> normal = [endX - startX, endY - startY];
final double magnitude = sqrt(normal[0] * normal[0] + normal[1] * normal[1]);
final List<double> un = [normal[0] / magnitude, normal[1] / magnitude];
return un;
}
}