Skip to content
Merged
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
1 change: 1 addition & 0 deletions packages/dragon_charts_flutter/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
> Note: This release has breaking changes.

- **BREAKING** **CHORE**: unify Dart SDK (^3.9.0) and Flutter (>=3.35.0 <3.36.0) constraints across workspace.
- **FEAT**: allow sparkline charts to customize the baseline calculation for positive/negative value classification, defaulting to the initial value.

## 0.1.1-dev.2

Expand Down
139 changes: 79 additions & 60 deletions packages/dragon_charts_flutter/lib/src/sparkline/sparkline_chart.dart
Original file line number Diff line number Diff line change
@@ -1,20 +1,45 @@
import 'package:flutter/material.dart';

typedef SparklineBaselineCalculator = double Function(List<double> data);

class SparklineBaselines {
const SparklineBaselines._();

static double initialValue(List<double> data) {
if (data.isEmpty) {
return 0;
}

return data.first;
}

static double average(List<double> data) {
if (data.isEmpty) {
return 0;
}

return data.reduce((a, b) => a + b) / data.length;
}
}

class SparklineChart extends StatelessWidget {
const SparklineChart({
required this.data,
required this.positiveLineColor,
required this.negativeLineColor,
required this.lineThickness,
this.isCurved = false,
SparklineBaselineCalculator? baselineCalculator,
super.key,
});
}) : baselineCalculator =
baselineCalculator ?? SparklineBaselines.initialValue;

final List<double> data;
final Color positiveLineColor;
final Color negativeLineColor;
final double lineThickness;
final bool isCurved;
final SparklineBaselineCalculator baselineCalculator;

@override
Widget build(BuildContext context) {
Expand All @@ -28,6 +53,7 @@ class SparklineChart extends StatelessWidget {
negativeLineColor: negativeLineColor,
lineThickness: lineThickness,
isCurved: isCurved,
baselineCalculator: baselineCalculator,
),
);
},
Expand All @@ -42,70 +68,68 @@ class _CustomSparklinePainter extends CustomPainter {
required this.negativeLineColor,
required this.lineThickness,
required this.isCurved,
}) {
// Handle empty data
if (data.isEmpty) {
average = 0;
} else {
average = data.reduce((a, b) => a + b) / data.length;
}
}
required SparklineBaselineCalculator baselineCalculator,
}) : baseline = data.isEmpty ? 0 : baselineCalculator(data);

final List<double> data;
final Color positiveLineColor;
final Color negativeLineColor;
final double lineThickness;
final bool isCurved;
late double average;
final double baseline;

@override
void paint(Canvas canvas, Size size) {
// Handle empty data
if (data.isEmpty) return;

// Handle single data point
if (data.length == 1) {
// Draw a horizontal line at the middle of the canvas
final Paint paint = Paint()
..color = data[0] >= 0 ? positiveLineColor : negativeLineColor
..color = data[0] >= baseline ? positiveLineColor : negativeLineColor
..strokeWidth = lineThickness
..style = PaintingStyle.stroke;

canvas.drawLine(
Offset(0, size.height / 2),
Offset(size.width, size.height / 2),
paint,
);
return;
}

final double dx = size.width / (data.length - 1);
final double minValue = data.reduce((a, b) => a < b ? a : b);
final double maxValue = data.reduce((a, b) => a > b ? a : b);

// Handle case where all values are the same
if (maxValue == minValue) {
// Draw a horizontal line at the middle of the canvas
final Paint paint = Paint()
..color = data[0] >= average ? positiveLineColor : negativeLineColor
..color = data[0] >= baseline ? positiveLineColor : negativeLineColor
..strokeWidth = lineThickness
..style = PaintingStyle.stroke;

canvas.drawLine(
Offset(0, size.height / 2),
Offset(size.width, size.height / 2),
paint,
);
return;
}

final double scaleY = size.height / (maxValue - minValue);
final double yAvg = size.height - ((average - minValue) * scaleY);
final double clampedBaseline = baseline
.clamp(minValue, maxValue)
.toDouble();
final double yBaseline =
size.height - ((clampedBaseline - minValue) * scaleY);

final Path pathAbove = Path();
final Path pathBelow = Path();
pathAbove.moveTo(0, yAvg);
pathBelow.moveTo(0, yAvg);
pathAbove.moveTo(0, yBaseline);
pathBelow.moveTo(0, yBaseline);

Offset? prevPointAbove;
Offset? prevPointBelow;
Expand All @@ -115,18 +139,18 @@ class _CustomSparklinePainter extends CustomPainter {
final y = size.height - ((data[i] - minValue) * scaleY);
final currentPoint = Offset(x, y);

if (data[i] >= average) {
if (i > 0 && data[i - 1] < average) {
if (data[i] >= baseline) {
if (i > 0 && data[i - 1] < baseline) {
final xPrev = (i - 1) * dx;
// final yPrev = size.height - ((data[i - 1] - minValue) * scaleY);
final intersectionX =
xPrev + (dx * (average - data[i - 1]) / (data[i] - data[i - 1]));
xPrev + (dx * (baseline - data[i - 1]) / (data[i] - data[i - 1]));

pathBelow
..lineTo(intersectionX, yAvg)
..lineTo(intersectionX, yAvg);
pathAbove.moveTo(intersectionX, yAvg);
prevPointAbove = Offset(intersectionX, yAvg);
..lineTo(intersectionX, yBaseline)
..lineTo(intersectionX, yBaseline);
pathAbove.moveTo(intersectionX, yBaseline);
prevPointAbove = Offset(intersectionX, yBaseline);
}

if (isCurved && prevPointAbove != null) {
Expand All @@ -152,17 +176,17 @@ class _CustomSparklinePainter extends CustomPainter {
}
prevPointAbove = currentPoint;
} else {
if (i > 0 && data[i - 1] >= average) {
if (i > 0 && data[i - 1] >= baseline) {
final xPrev = (i - 1) * dx;
// final yPrev = size.height - ((data[i - 1] - minValue) * scaleY);
final intersectionX =
xPrev + (dx * (average - data[i - 1]) / (data[i] - data[i - 1]));
xPrev + (dx * (baseline - data[i - 1]) / (data[i] - data[i - 1]));

pathAbove
..lineTo(intersectionX, yAvg)
..lineTo(intersectionX, yAvg);
pathBelow.moveTo(intersectionX, yAvg);
prevPointBelow = Offset(intersectionX, yAvg);
..lineTo(intersectionX, yBaseline)
..lineTo(intersectionX, yBaseline);
pathBelow.moveTo(intersectionX, yBaseline);
prevPointBelow = Offset(intersectionX, yBaseline);
}

if (isCurved && prevPointBelow != null) {
Expand Down Expand Up @@ -191,10 +215,10 @@ class _CustomSparklinePainter extends CustomPainter {
}

// Extend the path to the right edge of the canvas
if (data.last >= average) {
pathAbove.lineTo(size.width, yAvg);
if (data.last >= baseline) {
pathAbove.lineTo(size.width, yBaseline);
} else {
pathBelow.lineTo(size.width, yAvg);
pathBelow.lineTo(size.width, yBaseline);
}

// Gradient Paints
Expand All @@ -206,9 +230,7 @@ class _CustomSparklinePainter extends CustomPainter {
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
).createShader(
Rect.fromPoints(Offset.zero, Offset(0, size.height)),
);
).createShader(Rect.fromPoints(Offset.zero, Offset(0, size.height)));

final Paint belowGradientPaint = Paint()
..shader = LinearGradient(
Expand All @@ -218,9 +240,7 @@ class _CustomSparklinePainter extends CustomPainter {
],
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
).createShader(
Rect.fromPoints(Offset.zero, Offset(0, size.height)),
);
).createShader(Rect.fromPoints(Offset.zero, Offset(0, size.height)));

// Draw the filled paths first
canvas
Expand All @@ -238,16 +258,15 @@ class _CustomSparklinePainter extends CustomPainter {
final x2 = (i + 1) * dx;
final y2 = size.height - ((data[i + 1] - minValue) * scaleY);

if (data[i] >= average && data[i + 1] >= average) {
if (data[i] >= baseline && data[i + 1] >= baseline) {
linePaint.color = positiveLineColor;
} else if (data[i] < average && data[i + 1] < average) {
} else if (data[i] < baseline && data[i + 1] < baseline) {
linePaint.color = negativeLineColor;
} else {
final intersectionX =
x1 + (dx * (average - data[i]) / (data[i + 1] - data[i]));
final yAvg = size.height - ((average - minValue) * scaleY);
x1 + (dx * (baseline - data[i]) / (data[i + 1] - data[i]));

if (data[i] >= average) {
if (data[i] >= baseline) {
linePaint.color = positiveLineColor;
if (isCurved) {
canvas.drawPath(
Expand All @@ -257,19 +276,19 @@ class _CustomSparklinePainter extends CustomPainter {
(x1 + intersectionX) / 2,
y1,
(x1 + intersectionX) / 2,
yAvg,
yBaseline,
intersectionX,
yAvg,
yBaseline,
),
linePaint,
);
linePaint.color = negativeLineColor;
canvas.drawPath(
Path()
..moveTo(intersectionX, yAvg)
..moveTo(intersectionX, yBaseline)
..cubicTo(
(intersectionX + x2) / 2,
yAvg,
yBaseline,
(intersectionX + x2) / 2,
y2,
x2,
Expand All @@ -280,12 +299,12 @@ class _CustomSparklinePainter extends CustomPainter {
} else {
canvas.drawLine(
Offset(x1, y1),
Offset(intersectionX, yAvg),
Offset(intersectionX, yBaseline),
linePaint,
);
linePaint.color = negativeLineColor;
canvas.drawLine(
Offset(intersectionX, yAvg),
Offset(intersectionX, yBaseline),
Offset(x2, y2),
linePaint,
);
Expand All @@ -300,19 +319,19 @@ class _CustomSparklinePainter extends CustomPainter {
(x1 + intersectionX) / 2,
y1,
(x1 + intersectionX) / 2,
yAvg,
yBaseline,
intersectionX,
yAvg,
yBaseline,
),
linePaint,
);
linePaint.color = positiveLineColor;
canvas.drawPath(
Path()
..moveTo(intersectionX, yAvg)
..moveTo(intersectionX, yBaseline)
..cubicTo(
(intersectionX + x2) / 2,
yAvg,
yBaseline,
(intersectionX + x2) / 2,
y2,
x2,
Expand All @@ -323,12 +342,12 @@ class _CustomSparklinePainter extends CustomPainter {
} else {
canvas.drawLine(
Offset(x1, y1),
Offset(intersectionX, yAvg),
Offset(intersectionX, yBaseline),
linePaint,
);
linePaint.color = positiveLineColor;
canvas.drawLine(
Offset(intersectionX, yAvg),
Offset(intersectionX, yBaseline),
Offset(x2, y2),
linePaint,
);
Expand Down
Loading
Loading