Skip to content

Commit 5f5dca0

Browse files
committed
Add support for customized overlay and label texts
1 parent 5677df0 commit 5f5dca0

File tree

5 files changed

+135
-91
lines changed

5 files changed

+135
-91
lines changed

example/lib/main.dart

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class MyApp extends StatefulWidget {
1717

1818
class _MyAppState extends State<MyApp> {
1919
List<CandleData> _data = MockDataTesla.candles;
20-
bool _darkMode = false;
20+
bool _darkMode = true;
2121
bool _showAverage = false;
2222

2323
@override
@@ -55,17 +55,24 @@ class _MyAppState extends State<MyApp> {
5555
child: InteractiveChart(
5656
candles: _data,
5757
style: ChartStyle(
58-
priceGainColor: Colors.purple,
59-
priceLossColor: Colors.deepPurple,
60-
volumeColor: Colors.pink,
61-
trendLineColor: Colors.teal,
62-
priceGridLineColor: Colors.green,
63-
priceLabelStyle: TextStyle(color: Colors.orange[800]!),
64-
dateLabelStyle: TextStyle(color: Colors.indigo),
65-
selectionHighlightColor: Colors.yellow.withOpacity(0.5),
66-
overlayBackgroundColor: Colors.blue[100]!.withOpacity(0.8),
67-
overlayTextStyle: TextStyle(color: Colors.blue[900]),
58+
priceGainColor: Colors.teal[200]!,
59+
priceLossColor: Colors.blueGrey,
60+
volumeColor: Colors.teal.withOpacity(0.8),
61+
trendLineColor: Colors.blueGrey[200]!,
62+
priceGridLineColor: Colors.blue[200]!,
63+
priceLabelStyle: TextStyle(color: Colors.blue[200]),
64+
timeLabelStyle: TextStyle(color: Colors.indigo),
65+
selectionHighlightColor: Colors.red.withOpacity(0.2),
66+
overlayBackgroundColor: Colors.red[900]!.withOpacity(0.6),
67+
overlayTextStyle: TextStyle(color: Colors.red[100]),
6868
),
69+
overlayInfo: (candle) => {
70+
"💎": "🤚 ",
71+
"高": "${candle.high?.toStringAsFixed(2)}",
72+
"低": "${candle.low?.toStringAsFixed(2)}",
73+
},
74+
timeLabel: (timestamp, count) => "📅",
75+
priceLabel: (price) => "${price.round()} 💎",
6976
),
7077
),
7178
),

lib/chart_painter.dart

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,27 @@ import 'package:flutter/widgets.dart';
55
import 'candle_data.dart';
66
import 'painter_params.dart';
77

8+
typedef TimeLabelGetter = String Function(int timestamp, int visibleDataCount);
9+
typedef PriceLabelGetter = String Function(double price);
10+
typedef OverlayInfoGetter = Map<String, String> Function(CandleData candle);
11+
812
class ChartPainter extends CustomPainter {
913
final PainterParams params;
14+
final TimeLabelGetter getTimeLabel;
15+
final PriceLabelGetter getPriceLabel;
16+
final OverlayInfoGetter getOverlayInfo;
1017

11-
ChartPainter(this.params);
18+
ChartPainter({
19+
required this.params,
20+
required this.getTimeLabel,
21+
required this.getPriceLabel,
22+
required this.getOverlayInfo,
23+
});
1224

1325
@override
1426
void paint(Canvas canvas, Size size) {
15-
// Draw date labels & price labels
16-
_drawDateLabels(canvas, params);
27+
// Draw time labels (dates) & price labels
28+
_drawTimeLabels(canvas, params);
1729
_drawPriceGridAndLabels(canvas, params);
1830

1931
// Draw prices, volumes & trend line
@@ -38,8 +50,8 @@ class ChartPainter extends CustomPainter {
3850
}
3951
}
4052

41-
void _drawDateLabels(canvas, PainterParams params) {
42-
// We draw one date label per 90 pixels of screen width
53+
void _drawTimeLabels(canvas, PainterParams params) {
54+
// We draw one time label per 90 pixels of screen width
4355
final lineCount = params.chartWidth ~/ 90;
4456
final gap = 1 / (lineCount + 1);
4557
for (int i = 1; i <= lineCount; i++) {
@@ -48,15 +60,15 @@ class ChartPainter extends CustomPainter {
4860
if (index < params.candles.length) {
4961
final candle = params.candles[index];
5062
final visibleDataCount = params.candles.length;
51-
final dateTp = TextPainter(
63+
final timeTp = TextPainter(
5264
text: TextSpan(
53-
text: params.getDateLabel(candle.timestamp, visibleDataCount),
54-
style: params.style.dateLabelStyle,
65+
text: getTimeLabel(candle.timestamp, visibleDataCount),
66+
style: params.style.timeLabelStyle,
5567
),
5668
)
5769
..textDirection = TextDirection.ltr
5870
..layout();
59-
dateTp.paint(canvas, Offset(x - dateTp.width / 2, params.chartHeight));
71+
timeTp.paint(canvas, Offset(x - timeTp.width / 2, params.chartHeight));
6072
}
6173
}
6274
}
@@ -74,7 +86,7 @@ class ChartPainter extends CustomPainter {
7486
);
7587
final priceTp = TextPainter(
7688
text: TextSpan(
77-
text: params.getPriceLabel(y),
89+
text: getPriceLabel(y),
7890
style: params.style.priceLabelStyle,
7991
),
8092
)
@@ -200,16 +212,20 @@ class ChartPainter extends CustomPainter {
200212
..textDirection = TextDirection.ltr
201213
..layout();
202214

203-
final info = params.getOverlayInfo(candle);
215+
final info = getOverlayInfo(candle);
204216
final labels = info.keys.map((text) => makeTP(text)).toList();
205217
final values = info.values.map((text) => makeTP(text)).toList();
206218

207219
final labelsMaxWidth = labels.map((tp) => tp.width).reduce(max);
208220
final valuesMaxWidth = values.map((tp) => tp.width).reduce(max);
209221
final panelWidth = labelsMaxWidth + valuesMaxWidth + xGap * 3;
210-
final panelHeight =
211-
values.first.height * values.length + yGap * (values.length + 1);
222+
final panelHeight = max(
223+
labels.map((tp) => tp.height).reduce((a, b) => a + b),
224+
values.map((tp) => tp.height).reduce((a, b) => a + b),
225+
) +
226+
yGap * (values.length + 1);
212227

228+
// Shift the canvas, so the overlay panel can appear near touch position.
213229
canvas.save();
214230
final pos = params.tapPosition!;
215231
final fingerSize = 32.0; // leave some margin around user's finger
@@ -226,27 +242,33 @@ class ChartPainter extends CustomPainter {
226242
if (dy < 0) dy = 0.0;
227243
canvas.translate(dx, dy);
228244

229-
// Paint overlay panel and texts
245+
// Draw the background for overlay panel
230246
canvas.drawRRect(
231247
RRect.fromRectAndRadius(
232248
Offset.zero & Size(panelWidth, panelHeight),
233249
Radius.circular(8),
234250
),
235251
Paint()..color = params.style.overlayBackgroundColor);
252+
253+
// Draw texts
254+
var y = 0.0;
236255
for (int i = 0; i < labels.length; i++) {
237-
labels[i].paint(
238-
canvas,
239-
Offset(xGap, (yGap + values.first.height) * i + yGap),
240-
);
241-
}
242-
for (int i = 0; i < values.length; i++) {
243-
final leading = valuesMaxWidth - values[i].width;
256+
y += yGap;
257+
final rowHeight = max(labels[i].height, values[i].height);
258+
// Draw labels (left align, vertical center)
259+
final labelY = y + (rowHeight - labels[i].height) / 2; // vertical center
260+
labels[i].paint(canvas, Offset(xGap, labelY));
261+
262+
// Draw values (right align, vertical center)
263+
final leading = valuesMaxWidth - values[i].width; // right align
264+
final valueY = y + (rowHeight - values[i].height) / 2; // vertical center
244265
values[i].paint(
245266
canvas,
246-
Offset(labelsMaxWidth + xGap * 2 + leading,
247-
(yGap + values.first.height) * i + yGap),
267+
Offset(labelsMaxWidth + xGap * 2 + leading, valueY),
248268
);
269+
y += rowHeight;
249270
}
271+
250272
canvas.restore();
251273
}
252274

lib/chart_style.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import 'package:flutter/material.dart';
33
class ChartStyle {
44
final double volumeHeightFactor;
55
final double priceLabelWidth;
6-
final double dateLabelHeight;
6+
final double timeLabelHeight;
77

8-
final TextStyle dateLabelStyle;
8+
final TextStyle timeLabelStyle;
99
final TextStyle priceLabelStyle;
1010
final TextStyle overlayTextStyle;
1111

@@ -27,8 +27,8 @@ class ChartStyle {
2727
const ChartStyle({
2828
this.volumeHeightFactor = 0.2,
2929
this.priceLabelWidth = 48.0,
30-
this.dateLabelHeight = 24.0,
31-
this.dateLabelStyle = const TextStyle(
30+
this.timeLabelHeight = 24.0,
31+
this.timeLabelStyle = const TextStyle(
3232
fontSize: 16,
3333
color: Colors.grey,
3434
),

lib/interactive_chart.dart

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

33
import 'package:flutter/widgets.dart';
4+
import 'package:intl/intl.dart' as intl;
45

56
import 'candle_data.dart';
67
import 'chart_painter.dart';
@@ -11,8 +12,18 @@ class InteractiveChart extends StatefulWidget {
1112
final List<CandleData> candles;
1213
final ChartStyle style;
1314

14-
const InteractiveChart({Key? key, required this.candles, ChartStyle? style})
15-
: this.style = style ?? const ChartStyle(),
15+
final TimeLabelGetter? timeLabel;
16+
final PriceLabelGetter? priceLabel;
17+
final OverlayInfoGetter? overlayInfo;
18+
19+
const InteractiveChart({
20+
Key? key,
21+
required this.candles,
22+
ChartStyle? style,
23+
this.timeLabel,
24+
this.priceLabel,
25+
this.overlayInfo,
26+
}) : this.style = style ?? const ChartStyle(),
1627
assert(candles.length >= 3,
1728
"InteractiveChart requires 3 or more CandleData"),
1829
super(key: key);
@@ -120,7 +131,12 @@ class _InteractiveChartState extends State<InteractiveChart> {
120131
builder: (_, PainterParams params, __) {
121132
return CustomPaint(
122133
size: size,
123-
painter: ChartPainter(params),
134+
painter: ChartPainter(
135+
params: params,
136+
getTimeLabel: widget.timeLabel ?? defaultTimeLabel,
137+
getPriceLabel: widget.priceLabel ?? defaultPriceLabel,
138+
getOverlayInfo: widget.overlayInfo ?? defaultOverlayInfo,
139+
),
124140
);
125141
},
126142
),
@@ -186,4 +202,51 @@ class _InteractiveChartState extends State<InteractiveChart> {
186202
final start = widget.candles.length - count;
187203
return max(0, candleWidth * start);
188204
}
205+
206+
String defaultTimeLabel(int timestamp, int visibleDataCount) {
207+
final date = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000)
208+
.toIso8601String()
209+
.split("T")
210+
.first
211+
.split("-");
212+
213+
if (visibleDataCount > 20) {
214+
// If more than 20 data points are visible, we should show year and month.
215+
return "${date[0]}-${date[1]}"; // yyyy-mm
216+
} else {
217+
// Otherwise, we should show month and date.
218+
return "${date[1]}-${date[2]}"; // mm-dd
219+
}
220+
}
221+
222+
String defaultPriceLabel(double price) => price.toStringAsFixed(2);
223+
224+
Map<String, String> defaultOverlayInfo(CandleData candle) {
225+
final date = intl.DateFormat.yMMMd()
226+
.format(DateTime.fromMillisecondsSinceEpoch(candle.timestamp * 1000));
227+
return {
228+
"Date": date,
229+
"Open": candle.open?.toStringAsFixed(2) ?? "-",
230+
"High": candle.high?.toStringAsFixed(2) ?? "-",
231+
"Low": candle.low?.toStringAsFixed(2) ?? "-",
232+
"Close": candle.close?.toStringAsFixed(2) ?? "-",
233+
"Volume": candle.volume?.asAbbreviated() ?? "-",
234+
};
235+
}
236+
}
237+
238+
extension Formatting on double {
239+
String asPercent() {
240+
final format = this < 100 ? "##0.00" : "#,###";
241+
final v = intl.NumberFormat(format, "en_US").format(this);
242+
return "${this >= 0 ? '+' : ''}$v%";
243+
}
244+
245+
String asAbbreviated() {
246+
if (this < 1000) return this.toStringAsFixed(3);
247+
if (this >= 1e18) return this.toStringAsExponential(3);
248+
final s = intl.NumberFormat("#,###", "en_US").format(this).split(",");
249+
const suffixes = ["K", "M", "B", "T", "Q"];
250+
return "${s[0]}.${s[1]}${suffixes[s.length - 2]}";
251+
}
189252
}

lib/painter_params.dart

Lines changed: 2 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import 'dart:ui';
22
import 'package:flutter/widgets.dart';
3-
import 'package:intl/intl.dart' as intl;
43

54
import 'chart_style.dart';
65
import 'candle_data.dart';
@@ -41,8 +40,8 @@ class PainterParams {
4140
double get chartWidth => // width without price labels
4241
size.width - style.priceLabelWidth;
4342

44-
double get chartHeight => // height without date labels
45-
size.height - style.dateLabelHeight;
43+
double get chartHeight => // height without time labels
44+
size.height - style.timeLabelHeight;
4645

4746
double get volumeHeight => chartHeight * style.volumeHeightFactor;
4847

@@ -65,37 +64,6 @@ class PainterParams {
6564
return volumeHeight - vol + priceHeight - baseAmount;
6665
}
6766

68-
String getDateLabel(int timestamp, int visibleDataCount) {
69-
final date = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000)
70-
.toIso8601String()
71-
.split("T")
72-
.first
73-
.split("-");
74-
75-
if (visibleDataCount > 20) {
76-
// If more than 20 data points are visible, we should show year and month.
77-
return "${date[0]}-${date[1]}"; // yyyy-mm
78-
} else {
79-
// Otherwise, we should show month and date.
80-
return "${date[1]}-${date[2]}"; // mm-dd
81-
}
82-
}
83-
84-
String getPriceLabel(double price) => price.toStringAsFixed(2);
85-
86-
Map<String, String> getOverlayInfo(CandleData candle) {
87-
final date = intl.DateFormat.yMMMd()
88-
.format(DateTime.fromMillisecondsSinceEpoch(candle.timestamp * 1000));
89-
return {
90-
"Date": date,
91-
"Open": candle.open?.toStringAsFixed(2) ?? "-",
92-
"High": candle.high?.toStringAsFixed(2) ?? "-",
93-
"Low": candle.low?.toStringAsFixed(2) ?? "-",
94-
"Close": candle.close?.toStringAsFixed(2) ?? "-",
95-
"Volume": candle.volume?.asAbbreviated() ?? "-",
96-
};
97-
}
98-
9967
static PainterParams lerp(PainterParams a, PainterParams b, double t) {
10068
double lerpField(double getField(PainterParams p)) =>
10169
lerpDouble(getField(a), getField(b), t)!;
@@ -126,19 +94,3 @@ class PainterParamsTween extends Tween<PainterParams> {
12694
@override
12795
PainterParams lerp(double t) => PainterParams.lerp(begin ?? end!, end!, t);
12896
}
129-
130-
extension Formatting on double {
131-
String asPercent() {
132-
final format = this < 100 ? "##0.00" : "#,###";
133-
final v = intl.NumberFormat(format, "en_US").format(this);
134-
return "${this >= 0 ? '+' : ''}$v%";
135-
}
136-
137-
String asAbbreviated() {
138-
if (this < 1000) return this.toStringAsFixed(3);
139-
if (this >= 1e18) return this.toStringAsExponential(3);
140-
final s = intl.NumberFormat("#,###", "en_US").format(this).split(",");
141-
const suffixes = ["K", "M", "B", "T", "Q"];
142-
return "${s[0]}.${s[1]}${suffixes[s.length - 2]}";
143-
}
144-
}

0 commit comments

Comments
 (0)