Skip to content

Commit ba8c057

Browse files
committed
Add support for multiple trend lines
1 parent 3ec0fea commit ba8c057

File tree

7 files changed

+200
-631
lines changed

7 files changed

+200
-631
lines changed

example/lib/main.dart

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ class _MyAppState extends State<MyApp> {
3939
onPressed: () {
4040
setState(() => _showAverage = !_showAverage);
4141
if (_showAverage) {
42-
CandleData.computeMA(_data, 7);
42+
_computeTrendLines();
4343
} else {
44-
CandleData.deleteMA(_data);
44+
_removeTrendLines();
4545
}
4646
},
4747
),
@@ -59,7 +59,16 @@ class _MyAppState extends State<MyApp> {
5959
// priceGainColor: Colors.teal[200]!,
6060
// priceLossColor: Colors.blueGrey,
6161
// volumeColor: Colors.teal.withOpacity(0.8),
62-
// trendLineColor: Colors.blueGrey[200]!,
62+
// trendLineStyles: [
63+
// Paint()
64+
// ..strokeWidth = 2.0
65+
// ..strokeCap = StrokeCap.round
66+
// ..color = Colors.deepOrange,
67+
// Paint()
68+
// ..strokeWidth = 4.0
69+
// ..strokeCap = StrokeCap.round
70+
// ..color = Colors.orange,
71+
// ],
6372
// priceGridLineColor: Colors.blue[200]!,
6473
// priceLabelStyle: TextStyle(color: Colors.blue[200]),
6574
// timeLabelStyle: TextStyle(color: Colors.blue[200]),
@@ -86,4 +95,20 @@ class _MyAppState extends State<MyApp> {
8695
),
8796
);
8897
}
98+
99+
_computeTrendLines() {
100+
final ma7 = CandleData.computeMA(_data, 7);
101+
final ma30 = CandleData.computeMA(_data, 30);
102+
final ma90 = CandleData.computeMA(_data, 90);
103+
104+
for (int i = 0; i < _data.length; i++) {
105+
_data[i].trends = [ma7[i], ma30[i], ma90[i]];
106+
}
107+
}
108+
109+
_removeTrendLines() {
110+
for (final data in _data) {
111+
data.trends = [];
112+
}
113+
}
89114
}

example/lib/mock_data.dart

Lines changed: 95 additions & 575 deletions
Large diffs are not rendered by default.

lib/src/candle_data.dart

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,18 @@ class CandleData {
2323
/// The volume information of this data point.
2424
final double? volume;
2525

26-
/// Optional data holder for drawing a trend line, e.g. moving average.
27-
double? trend;
26+
/// Data holder for additional trend lines, for this data point.
27+
///
28+
/// For a single trend line, we can assign it as a list with a single element.
29+
/// For example if we want "7 days moving average", do something like
30+
/// `trends = [ma7]`. If there are multiple tread lines, we can assign a list
31+
/// with multiple elements, like `trends = [ma7, ma30]`.
32+
/// If we don't want any trend lines, we can assign an empty list.
33+
///
34+
/// This should be an unmodifiable list, so please do not use `add`
35+
/// or `clear` methods on the list. Always assign a new list if values
36+
/// are changed. Otherwise the UI might not be updated.
37+
List<double?> trends;
2838

2939
CandleData({
3040
required this.timestamp,
@@ -33,26 +43,32 @@ class CandleData {
3343
required this.volume,
3444
this.high,
3545
this.low,
36-
});
46+
List<double?>? trends,
47+
}) : this.trends = List.unmodifiable(trends ?? []);
3748

38-
static void computeMA(List<CandleData> data, [int period = 7]) {
39-
if (data.length < period * 2) return;
49+
static List<double?> computeMA(List<CandleData> data, [int period = 7]) {
50+
// If data is not at least twice as long as the period, return nulls.
51+
if (data.length < period * 2) return List.filled(data.length, null);
52+
53+
final List<double?> result = [];
54+
// Skip the first [period] data points. For example, skip 7 data points.
4055
final firstPeriod =
4156
data.take(period).map((d) => d.close).whereType<double>();
4257
double ma = firstPeriod.reduce((a, b) => a + b) / firstPeriod.length;
58+
result.addAll(List.filled(period, null));
4359

60+
// Compute the moving average for the rest of the data points.
4461
for (int i = period; i < data.length; i++) {
4562
final curr = data[i].close;
4663
final prev = data[i - period].close;
4764
if (curr != null && prev != null) {
4865
ma = (ma * period + curr - prev) / period;
49-
data[i].trend = ma;
66+
result.add(ma);
67+
} else {
68+
result.add(null);
5069
}
5170
}
52-
}
53-
54-
static void deleteMA(List<CandleData> data) {
55-
data.forEach((element) => element.trend = null);
71+
return result;
5672
}
5773

5874
@override

lib/src/chart_painter.dart

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'dart:math';
22

3+
import 'package:flutter/material.dart';
34
import 'package:flutter/widgets.dart';
45

56
import 'candle_data.dart';
@@ -150,39 +151,44 @@ class ChartPainter extends CustomPainter {
150151
);
151152
}
152153
// Draw trend line
153-
final trendLinePaint = Paint()
154-
..strokeWidth = 2.0
155-
..strokeCap = StrokeCap.round
156-
..color = params.style.trendLineColor;
157-
final pt = candle.trend; // current data point
158-
final prevPt = params.candles.at(i - 1)?.trend;
159-
if (pt != null && prevPt != null) {
160-
canvas.drawLine(
161-
Offset(x - params.candleWidth, params.fitPrice(prevPt)),
162-
Offset(x, params.fitPrice(pt)),
163-
trendLinePaint,
164-
);
165-
}
166-
if (i == 0) {
167-
// In the front, draw an extra line connecting to out-of-window data
168-
if (pt != null && params.leadingTrend != null) {
154+
for (int j = 0; j < candle.trends.length; j++) {
155+
final trendLinePaint = params.style.trendLineStyles.at(j) ??
156+
(Paint()
157+
..strokeWidth = 2.0
158+
..strokeCap = StrokeCap.round
159+
..color = Colors.blue);
160+
161+
final pt = candle.trends.at(j); // current data point
162+
final prevPt = params.candles.at(i - 1)?.trends.at(j);
163+
if (pt != null && prevPt != null) {
169164
canvas.drawLine(
170-
Offset(x - params.candleWidth, params.fitPrice(params.leadingTrend!)),
165+
Offset(x - params.candleWidth, params.fitPrice(prevPt)),
171166
Offset(x, params.fitPrice(pt)),
172167
trendLinePaint,
173168
);
174169
}
175-
} else if (i == params.candles.length - 1) {
176-
// At the end, draw an extra line connecting to out-of-window data
177-
if (pt != null && params.trailingTrend != null) {
178-
canvas.drawLine(
179-
Offset(x, params.fitPrice(pt)),
180-
Offset(
181-
x + params.candleWidth,
182-
params.fitPrice(params.trailingTrend!),
183-
),
184-
trendLinePaint,
185-
);
170+
if (i == 0) {
171+
// In the front, draw an extra line connecting to out-of-window data
172+
if (pt != null && params.leadingTrends?.at(j) != null) {
173+
canvas.drawLine(
174+
Offset(x - params.candleWidth,
175+
params.fitPrice(params.leadingTrends!.at(j)!)),
176+
Offset(x, params.fitPrice(pt)),
177+
trendLinePaint,
178+
);
179+
}
180+
} else if (i == params.candles.length - 1) {
181+
// At the end, draw an extra line connecting to out-of-window data
182+
if (pt != null && params.trailingTrends?.at(j) != null) {
183+
canvas.drawLine(
184+
Offset(x, params.fitPrice(pt)),
185+
Offset(
186+
x + params.candleWidth,
187+
params.fitPrice(params.trailingTrends!.at(j)!),
188+
),
189+
trendLinePaint,
190+
);
191+
}
186192
}
187193
}
188194
}

lib/src/chart_style.dart

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,10 @@ class ChartStyle {
3838
/// The color of the `volume` bars.
3939
final Color volumeColor;
4040

41-
/// The color of the trend line, if available.
42-
final Color trendLineColor;
41+
/// The style of trend lines. If there are multiple lines, their styles will
42+
/// be chosen in the order of appearance in this list. If this list is shorter
43+
/// than the number of trend lines, a default blue paint will be applied.
44+
final List<Paint> trendLineStyles;
4345

4446
/// The color of the price grid line.
4547
final Color priceGridLineColor;
@@ -71,7 +73,7 @@ class ChartStyle {
7173
this.priceGainColor = Colors.green,
7274
this.priceLossColor = Colors.red,
7375
this.volumeColor = Colors.grey,
74-
this.trendLineColor = Colors.blue,
76+
this.trendLineStyles = const [],
7577
this.priceGridLineColor = Colors.grey,
7678
this.selectionHighlightColor = const Color(0x33757575),
7779
this.overlayBackgroundColor = const Color(0xEE757575),

lib/src/interactive_chart.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,8 @@ class _InteractiveChartState extends State<InteractiveChart> {
111111

112112
// If possible, find neighbouring trend line data,
113113
// so the chart could draw better-connected lines
114-
final leadingTrend = widget.candles.at(start - 1)?.trend;
115-
final trailingTrend = widget.candles.at(end + 1)?.trend;
114+
final leadingTrends = widget.candles.at(start - 1)?.trends;
115+
final trailingTrends = widget.candles.at(end + 1)?.trends;
116116

117117
// Find the horizontal shift needed when drawing the candles.
118118
// First, always shift the chart by half a candle, because when we
@@ -179,8 +179,8 @@ class _InteractiveChartState extends State<InteractiveChart> {
179179
minVol: minVol,
180180
xShift: xShift,
181181
tapPosition: _tapPosition,
182-
leadingTrend: leadingTrend,
183-
trailingTrend: trailingTrend,
182+
leadingTrends: leadingTrends,
183+
trailingTrends: trailingTrends,
184184
),
185185
),
186186
duration: Duration(milliseconds: 300),

lib/src/painter_params.dart

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ class PainterParams {
1818

1919
final double xShift;
2020
final Offset? tapPosition;
21-
final double? leadingTrend;
22-
final double? trailingTrend;
21+
final List<double?>? leadingTrends;
22+
final List<double?>? trailingTrends;
2323

2424
PainterParams({
2525
required this.candles,
@@ -33,8 +33,8 @@ class PainterParams {
3333
required this.minVol,
3434
required this.xShift,
3535
required this.tapPosition,
36-
required this.leadingTrend,
37-
required this.trailingTrend,
36+
required this.leadingTrends,
37+
required this.trailingTrends,
3838
});
3939

4040
double get chartWidth => // width without price labels
@@ -79,8 +79,8 @@ class PainterParams {
7979
minVol: lerpField((p) => p.minVol),
8080
xShift: b.xShift,
8181
tapPosition: b.tapPosition,
82-
leadingTrend: b.leadingTrend,
83-
trailingTrend: b.trailingTrend,
82+
leadingTrends: b.leadingTrends,
83+
trailingTrends: b.trailingTrends,
8484
);
8585
}
8686

@@ -99,8 +99,8 @@ class PainterParams {
9999

100100
if (tapPosition != other.tapPosition) return true;
101101

102-
if (leadingTrend != other.leadingTrend ||
103-
trailingTrend != other.trailingTrend) return true;
102+
if (leadingTrends != other.leadingTrends ||
103+
trailingTrends != other.trailingTrends) return true;
104104

105105
if (style != other.style) return true;
106106

0 commit comments

Comments
 (0)