Line data Source code
1 : import 'dart:math';
2 :
3 : import 'package:fl_animated_linechart/chart/datetime_chart_point.dart';
4 : import 'package:fl_animated_linechart/chart/highlight_point.dart';
5 : import 'package:fl_animated_linechart/chart/line_chart.dart';
6 : import 'package:fl_animated_linechart/common/text_direction_helper.dart';
7 : import 'package:flutter/material.dart';
8 : import 'package:flutter/widgets.dart';
9 : import 'package:intl/intl.dart';
10 :
11 : class AnimatedLineChart extends StatefulWidget {
12 :
13 : final LineChart chart;
14 :
15 2 : const AnimatedLineChart(this.chart, {Key key, }) : super(key: key);
16 :
17 1 : @override
18 1 : _AnimatedLineChartState createState() => _AnimatedLineChartState();
19 : }
20 :
21 : class _AnimatedLineChartState extends State<AnimatedLineChart> with SingleTickerProviderStateMixin {
22 : AnimationController _controller;
23 : Animation _animation;
24 :
25 1 : @override
26 : void initState() {
27 3 : _controller = AnimationController(vsync: this, duration: Duration(milliseconds: 600));
28 :
29 2 : Animation curve = CurvedAnimation(parent: _controller, curve: Curves.easeInOutExpo);
30 :
31 3 : _animation = Tween(begin: 0.0, end: 1.0).animate(curve);
32 :
33 2 : _controller.forward();
34 :
35 1 : super.initState();
36 : }
37 :
38 1 : @override
39 : void dispose() {
40 2 : _controller.dispose();
41 1 : super.dispose();
42 : }
43 :
44 1 : @override
45 : Widget build(BuildContext context) {
46 1 : return Padding(
47 1 : padding: EdgeInsets.only(right: ChartPainter.axisOffsetPX),
48 1 : child: LayoutBuilder(
49 1 : builder: (BuildContext context, BoxConstraints constraints) {
50 5 : widget.chart.initialize(constraints.maxWidth, constraints.maxHeight);
51 :
52 6 : return _GestureWrapper(constraints.maxHeight, constraints.maxWidth, widget.chart, _animation);
53 : }
54 : ),
55 : );
56 : }
57 : }
58 :
59 : //Wrap gestures, to avoid reinitializing the chart model when doing gestures
60 : class _GestureWrapper extends StatefulWidget {
61 : final double _height;
62 : final double width;
63 : final LineChart chart;
64 : final Animation animation;
65 :
66 2 : const _GestureWrapper(this._height, this.width, this.chart, this.animation, {Key key,}) : super(key: key);
67 :
68 1 : @override
69 1 : _GestureWrapperState createState() => _GestureWrapperState();
70 : }
71 :
72 : class _GestureWrapperState extends State<_GestureWrapper> {
73 : bool horizontalDragActive = false;
74 : double horizontalDragPosition = 0.0;
75 :
76 1 : @override
77 : Widget build(BuildContext context) {
78 1 : return GestureDetector(
79 11 : child: _AnimatedChart(widget.chart, widget.width, widget._height, horizontalDragActive, horizontalDragPosition, animation: widget.animation,),
80 1 : onHorizontalDragStart: (dragStartDetails) {
81 1 : horizontalDragActive = true;
82 3 : horizontalDragPosition = dragStartDetails.globalPosition.dx;
83 2 : setState(() {
84 : });
85 : },
86 0 : onHorizontalDragUpdate: (dragUpdateDetails) {
87 0 : horizontalDragPosition += dragUpdateDetails.primaryDelta;
88 0 : setState(() {
89 : });
90 : },
91 0 : onHorizontalDragEnd: (dragEndDetails) {
92 0 : horizontalDragActive = false;
93 0 : horizontalDragPosition = 0.0;
94 0 : setState(() {
95 : });
96 : },
97 : );
98 : }
99 : }
100 :
101 : class _AnimatedChart extends AnimatedWidget {
102 : final double height;
103 : final double width;
104 : final LineChart chart;
105 : final bool horizontalDragActive;
106 : final double horizontalDragPosition;
107 :
108 2 : _AnimatedChart(this.chart, this.width, this.height, this.horizontalDragActive, this.horizontalDragPosition, {Key key, Animation animation}) : super(key: key, listenable: animation);
109 :
110 1 : @override
111 : Widget build(BuildContext context) {
112 1 : Animation animation = listenable as Animation;
113 :
114 1 : return CustomPaint(
115 5 : painter: ChartPainter(animation?.value, chart, horizontalDragActive, horizontalDragPosition),
116 : );
117 : }
118 : }
119 :
120 : class ChartPainter extends CustomPainter {
121 :
122 : static final double axisOffsetPX = 50.0;
123 : static final double stepCount = 5;
124 :
125 : final DateFormat _formatMonthDayHoursMinutes = DateFormat('dd/MM kk:mm');
126 :
127 : final Paint _gridPainter = Paint()
128 : ..style = PaintingStyle.stroke
129 : ..strokeWidth = 1
130 : ..color = Colors.black26;
131 :
132 : Paint linePainter = Paint()
133 : ..style = PaintingStyle.stroke
134 : ..strokeWidth = 2
135 : ..color = Colors.black26;
136 :
137 : Paint tooltipPainter = Paint()
138 : ..style = PaintingStyle.fill
139 : ..color = Colors.white.withAlpha(230);
140 :
141 : final double progress;
142 : final LineChart chart;
143 : final bool horizontalDragActive;
144 : final double horizontalDragPosition;
145 :
146 1 : ChartPainter(this.progress, this.chart, this.horizontalDragActive, this.horizontalDragPosition);
147 :
148 1 : @override
149 : void paint(Canvas canvas, Size size) {
150 1 : _drawGrid(canvas, size);
151 1 : _drawUnit(canvas, size);
152 1 : _drawLines(size, canvas);
153 1 : _drawAxisValues(canvas, size);
154 :
155 1 : if (horizontalDragActive) {
156 1 : _drawHighlights(size, canvas);
157 : }
158 : }
159 :
160 1 : void _drawHighlights(Size size, Canvas canvas) {
161 2 : linePainter.color = Colors.black45;
162 :
163 5 : if (horizontalDragPosition > axisOffsetPX && horizontalDragPosition < size.width) {
164 8 : canvas.drawLine(Offset(horizontalDragPosition, 0), Offset(horizontalDragPosition, size.height - axisOffsetPX), linePainter);
165 : }
166 :
167 3 : List<HighlightPoint> highlights = chart.getClosetHighlightPoints(horizontalDragPosition);
168 1 : List<TextPainter> textPainters = List();
169 : int index = 0;
170 3 : double minHighlightX = highlights[0].chartPoint.x;
171 3 : double minHighlightY = highlights[0].chartPoint.y;
172 : double maxWidth = 0;
173 :
174 2 : highlights.forEach((highlight) {
175 3 : if (highlight.chartPoint.x < minHighlightX) {
176 0 : minHighlightX = highlight.chartPoint.x;
177 : }
178 3 : if (highlight.chartPoint.y < minHighlightY) {
179 0 : minHighlightY = highlight.chartPoint.y;
180 : }
181 : });
182 :
183 2 : highlights.forEach((highlight) {
184 7 : canvas.drawCircle(Offset(highlight.chartPoint.x, highlight.chartPoint.y), 5, linePainter);
185 :
186 : String prefix = "";
187 :
188 2 : if (highlight.chartPoint is DateTimeChartPoint) {
189 1 : DateTimeChartPoint dateTimeChartPoint = highlight.chartPoint;
190 3 : prefix = _formatMonthDayHoursMinutes.format(dateTimeChartPoint.dateTime);
191 : }
192 :
193 11 : TextSpan span = new TextSpan(style: new TextStyle(color: chart.lines[index].color, fontWeight: FontWeight.w200, fontSize: 12), text: '$prefix: ${highlight.yValue.toStringAsFixed(1)} ${chart.yAxisUnit}');
194 2 : TextPainter tp = new TextPainter(text: span, textAlign: TextAlign.right, textDirection: TextDirectionHelper.getDirection());
195 :
196 1 : tp.layout();
197 :
198 2 : if (tp.width > maxWidth) {
199 1 : maxWidth = tp.width;
200 : }
201 :
202 1 : textPainters.add(tp);
203 1 : index++;
204 : });
205 :
206 1 : minHighlightX += 12; //make room for the chart points
207 5 : double tooltipHeight = textPainters[0].height * textPainters.length + 16;
208 :
209 4 : if ((minHighlightX + maxWidth + 16) > size.width) {
210 1 : minHighlightX -= maxWidth;
211 1 : minHighlightX -= 34;
212 : }
213 :
214 6 : if (minHighlightY + tooltipHeight > size.height - chart.axisOffSetWithPadding) {
215 5 : minHighlightY = size.height - chart.axisOffSetWithPadding - tooltipHeight;
216 : }
217 :
218 : //Draw highlight bordered box:
219 4 : Rect tooltipRect = Rect.fromLTWH(minHighlightX-5, minHighlightY - 5, maxWidth+20, tooltipHeight);
220 2 : canvas.drawRect(tooltipRect, tooltipPainter);
221 2 : canvas.drawRect(tooltipRect, _gridPainter);
222 :
223 : //Draw the actual highlights:
224 2 : textPainters.forEach((tp) {
225 3 : tp.paint(canvas, Offset(minHighlightX+5, minHighlightY));
226 1 : minHighlightY += 17;
227 : });
228 : }
229 :
230 1 : void _drawAxisValues(Canvas canvas, Size size) {
231 : //TODO: calculate and cache
232 3 : for (int c = 0; c <= (stepCount + 1); c++) {
233 3 : TextPainter tp = chart.yAxisTexts[c];
234 13 : tp.paint(canvas, new Offset(chart.axisOffSetWithPadding - tp.width, (size.height - 6)- (c * chart.heightStepSize) - axisOffsetPX));
235 : }
236 :
237 : //TODO: calculate and cache
238 3 : for (int c = 0; c <= (stepCount + 1); c++) {
239 15 : _drawRotatedText(canvas, chart.xAxisTexts[c], chart.axisOffSetWithPadding + (c * chart.widthStepSize), size.height - chart.axisOffSetWithPadding, pi * 1.5);
240 : }
241 : }
242 :
243 1 : void _drawLines(Size size, Canvas canvas) {
244 : int index = 0;
245 :
246 4 : chart.lines.forEach((chartLine) {
247 3 : linePainter.color = chartLine.color;
248 : Path path;
249 :
250 3 : List<HighlightPoint> points = chart.seriesMap[index];
251 :
252 2 : bool drawCircles = points.length < 100;
253 :
254 2 : if (progress < 1.0) {
255 1 : path = Path(); // create new path, to make animation work
256 : bool init = true;
257 :
258 3 : chartLine.points.forEach((p) {
259 7 : double x = (p.x * chart.xScale) - chart.xOffset;
260 10 : double adjustedY = (p.y * chart.yScale) - (chart.minY * chart.yScale);
261 5 : double y = (size.height - LineChart.axisOffsetPX) - (adjustedY * progress);
262 :
263 : //adjust to make room for axis values:
264 1 : x += LineChart.axisOffsetPX;
265 :
266 : if (init) {
267 : init = false;
268 1 : path.moveTo(x, y);
269 : }
270 :
271 1 : path.lineTo(x, y);
272 : if (drawCircles) {
273 3 : canvas.drawCircle(Offset(x, y), 2, linePainter);
274 : }
275 : });
276 : } else {
277 2 : path = chart.getPathCache(index);
278 :
279 : if (drawCircles) {
280 9 : points.forEach((p) => canvas.drawCircle(Offset(p.chartPoint.x, p.chartPoint.y), 2, linePainter));
281 : }
282 : }
283 :
284 2 : canvas.drawPath(path, linePainter);
285 1 : index++;
286 : });
287 : }
288 :
289 1 : void _drawUnit(Canvas canvas, Size size) {
290 4 : TextSpan span = new TextSpan(style: new TextStyle(color: Colors.black54, fontWeight: FontWeight.w200, fontSize: 12), text: chart.yAxisUnit);
291 2 : TextPainter tp = new TextPainter(text: span, textAlign: TextAlign.right, textDirection: TextDirectionHelper.getDirection());
292 1 : tp.layout();
293 :
294 4 : tp.paint(canvas, new Offset(LineChart.axisOffsetPX - tp.width, -18));
295 : }
296 :
297 1 : void _drawGrid(Canvas canvas, Size size) {
298 7 : canvas.drawRect(Rect.fromLTWH(axisOffsetPX, 0, size.width - axisOffsetPX, size.height - axisOffsetPX), _gridPainter);
299 :
300 2 : for(double c = 1; c <= stepCount; c ++) {
301 11 : canvas.drawLine(Offset(axisOffsetPX, c*chart.heightStepSize), Offset(size.width, c*chart.heightStepSize), _gridPainter);
302 14 : canvas.drawLine(Offset(c*chart.widthStepSize + axisOffsetPX, 0), Offset(c*chart.widthStepSize + axisOffsetPX, size.height-axisOffsetPX), _gridPainter);
303 : }
304 : }
305 :
306 1 : void _drawRotatedText(Canvas canvas,TextPainter tp, double x, double y, double angleRotationInRadians) {
307 1 : canvas.save();
308 3 : canvas.translate(x, y + tp.width);
309 1 : canvas.rotate(angleRotationInRadians);
310 2 : tp.paint(canvas, new Offset(0.0,0.0));
311 1 : canvas.restore();
312 : }
313 :
314 1 : @override
315 : bool shouldRepaint(CustomPainter oldDelegate) {
316 : return true;
317 : }
318 : }
|