Flutter draw linechart
The project address
HiCoordinate
import 'package:flutter/material.dart';
import 'package:hi_chart/src/bean/chart_bean.dart';
import 'package:hi_chart/src/constant.dart' as constant;
import 'dart:math' as math;
import 'package:hi_chart/src/util/axis_util.dart';
typedef HiCoordinateBuilder = Widget Function(BuildContext context, int cormax, int cormin, double translateX);
class HiCoordinate extends StatefulWidget {
final HiCoordinateBuilder builder;
final double width;
final double height;
final List<ChartBean> data;
final Color axisColor;
final TextStyle labelStyle;
final double axisWidth;
final VoidCallback onHorizontalDrag;
const HiCoordinate({
@required this.data,
this.builder,
this.width,
this.height,
this.axisColor = constant.axisColor,
this.labelStyle,
this.axisWidth = 1,
this.onHorizontalDrag,
Key key,
}) : super(key: key);
@override
_HiCoordinateState createState() => _HiCoordinateState();
}
class _HiCoordinateState extends State<HiCoordinate> {
num maxValue; //最大值
num minValue; //最小值
int cormax, cormin; //优化后最大值、最小值
int step; //优化后步数大小
int cornumber; //优化后Y轴分割数
double translateX = 0; //偏移量
double maxTranslateX; //最大偏移量
double startX; //手势临时变量
double tempTranslateX; //手势临时变量
@override
void initState() {
super.initState();
initValue();
initAxisData();
}
initValue() {
minValue = widget.data[0].value;
maxValue = widget.data[0].value;
for (final item in widget.data) {
maxValue = math.max(item.value, maxValue);
minValue = math.min(item.value, minValue);
}
}
initAxisData() {
final yAxisData = AxisUtils.mathYAxis(maxValue, minValue, constant.cornumber);
cormax = yAxisData[0];
cormin = yAxisData[1];
cornumber = yAxisData[2];
step = yAxisData[3];
}
@override
Widget build(BuildContext context) {
return Container(
width: widget.width ?? double.infinity,
height: widget.height,
child: Stack(
children: [
Container(
width: double.infinity,
height: double.infinity,
child: CustomPaint(
painter: _HiCoordinatePainter(
data: widget.data,
cornumber: cornumber,
step: step,
axisColor: widget.axisColor,
axisWidth: widget.axisWidth,
labelStyle: widget.labelStyle,
translateX: translateX,
),
),
),
if (widget.builder != null)
Positioned.fill(
child: LayoutBuilder(
builder: (context, viewport) {
final out = (widget.data.length + 1) * constant.pointSpace - viewport.maxWidth;
maxTranslateX = math.max(0, out);
return GestureDetector(
onHorizontalDragStart: _onHorizontalDragStart,
onHorizontalDragUpdate: _onHorizontalDragUpdate,
onHorizontalDragEnd: _onHorizontalDragEnd,
child: widget.builder(context, cormax, cormin, translateX),
);
},
),
bottom: constant.xLabelHeight,
),
],
),
);
}
/*
更新偏移量
*/
void _onHorizontalDragStart(DragStartDetails details) {
startX = details.globalPosition.dx;
tempTranslateX = translateX;
}
void _onHorizontalDragUpdate(DragUpdateDetails details) {
final distanceX = details.globalPosition.dx - startX;
final tempX = tempTranslateX + distanceX;
if (tempX > 0 || tempX.abs() > maxTranslateX) return;
setState(() {
translateX = tempX;
});
if (widget.onHorizontalDrag != null) widget.onHorizontalDrag();
}
void _onHorizontalDragEnd(DragEndDetails details) {
startX = null;
tempTranslateX = null;
}
/*
更新偏移量
*/
}
class _HiCoordinatePainter extends CustomPainter {
final List<ChartBean> data;
final int cornumber;
final Color axisColor;
final double axisWidth;
final TextStyle labelStyle;
final double translateX;
final int step;
Paint axisPaint; //坐标轴画笔
_HiCoordinatePainter({
@required this.data,
@required this.cornumber,
@required this.step,
this.axisColor,
this.axisWidth,
this.labelStyle,
this.translateX = 0,
}) {
init();
}
void init() {
axisPaint = Paint()
..color = axisColor
..strokeWidth = axisWidth
..style = PaintingStyle.stroke;
}
@override
void paint(Canvas canvas, Size size) {
//调整画布为正常笛卡尔坐标系
canvas.save();
canvas.scale(1, -1);
canvas.translate(0, -size.height + constant.xLabelHeight);
final axisSize = Size(size.width, size.height - constant.xLabelHeight);
drawAxis(canvas, axisSize);
drawAxisLabel(canvas, axisSize);
canvas.restore();
}
//绘制坐标轴
void drawAxis(Canvas canvas, Size size) {
//绘制X轴
final xAxisPath = Path();
xAxisPath.lineTo(size.width, 0);
canvas.drawPath(xAxisPath, axisPaint);
//绘制Y轴
final yAxisPath = Path();
yAxisPath.lineTo(0, size.height);
canvas.drawPath(yAxisPath, axisPaint);
}
//绘制坐标轴标签
void drawAxisLabel(Canvas canvas, Size size) {
canvas.save();
canvas.scale(1, -1);
canvas.clipRect(Rect.fromLTWH(constant.pointSpace / 2, -constant.xLabelHeight, size.width - constant.pointSpace / 2, size.height));
final labelTextStyle = labelStyle ?? TextStyle(fontSize: 14, color: axisColor);
for (final item in data) {
final index = data.indexOf(item) + 1;
final offset = Offset(index * constant.pointSpace + translateX, 2);
TextPainter(
text: TextSpan(
text: item.title,
style: labelTextStyle,
),
textDirection: TextDirection.ltr,
textAlign: TextAlign.center,
)
..layout(minWidth: constant.pointSpace, maxWidth: constant.pointSpace)
..paint(canvas, offset);
}
canvas.restore();
canvas.save();
canvas.scale(1, -1);
final ySpace = size.height / cornumber;
for (int i = 0; i <= cornumber; i++) {
final labelNumber = i * step;
final offset = Offset(5, -ySpace * i);
TextPainter(
text: TextSpan(text: '$labelNumber', style: labelTextStyle),
textDirection: TextDirection.ltr,
)
..layout(minWidth: constant.yLabelWidth, maxWidth: constant.yLabelWidth)
..paint(canvas, offset);
}
canvas.restore();
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
Copy the code
This widget is responsible for drawing the coordinate system and serving as the host for the content
HiBarChart
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:hi_chart/src/bean/chart_bean.dart';
import 'package:hi_chart/src/constant.dart' as constant;
import 'package:hi_chart/src/util/util.dart';
import 'package:hi_chart/src/widget/hi_coordinate.dart';
class HiBarChart extends StatefulWidget {
final List<ChartBean> data; //数据集
final double width; //宽度
final double height; //高度
final Color lineColor; //线、点颜色
final Color axisColor; //坐标轴颜色
final TextStyle axisLabelStyle; //坐标轴标签字体样式
final double axisWidth; // 坐标轴宽度
final double lineWidth; //线宽度
const HiBarChart({
@required this.data,
this.width = double.infinity,
this.height,
this.axisColor = const Color(0xffe6e6e6),
this.lineColor,
this.axisLabelStyle,
this.axisWidth = 1,
this.lineWidth = 1,
Key key,
}) : super(key: key);
@override
_HiBarChart createState() => _HiBarChart();
}
class _HiBarChart extends State<HiBarChart> with TickerProviderStateMixin {
List<Rect> rects = []; //点集合
String tipTitle; //提示标题
String tipValue; //提示数据值
double tipTop; //提示顶部距离
double tipLeft; //提示底部距离
bool isShowTip = false; //是否显示提示
Timer tipTimer; //自动关闭提示
GlobalKey tipKey = GlobalKey(); //提示key
GlobalKey contentKey = GlobalKey(); //内容区域
AnimationController initAnimationController; //初始化动画
AnimationController changeAnimationController; //数据变更动画
List<DiffValue> changeList; //数据变更数据集
@override
void initState() {
super.initState();
initAnimationController = AnimationController(duration: Duration(seconds: 1), vsync: this)
..addListener(() {
setState(() {});
});
changeAnimationController = AnimationController(duration: Duration(milliseconds: 300), vsync: this)
..addListener(() {
setState(() {});
});
initAnimationController.forward();
}
@override
Widget build(BuildContext context) {
return HiCoordinate(
data: widget.data,
width: widget.width,
height: widget.height,
axisColor: widget.axisColor,
axisWidth: widget.axisWidth,
labelStyle: widget.axisLabelStyle,
onHorizontalDrag: () {
hideTip();
},
builder: (context, cormax, cormin, translateX) => GestureDetector(
onTapUp: _onTapUp,
child: Stack(
overflow: Overflow.visible,
children: [
Positioned.fill(
child: Container(
child: CustomPaint(
key: contentKey,
willChange: true,
painter: _HiBarChartPaint(
context: context,
data: widget.data,
translateX: translateX,
cormax: cormax,
cormin: cormin,
lineColor: widget.lineColor,
lineWidth: widget.lineWidth,
onRectsChanged: (rects) {
this.rects = rects;
},
progress: initAnimationController.value,
changeList: changeList,
changeProgress: changeAnimationController.value,
),
),
),
),
Positioned(
top: isShowTip ? tipTop : -99999,
left: tipLeft,
child: Container(
key: tipKey,
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
constraints: BoxConstraints(minWidth: 60),
decoration: BoxDecoration(
color: Colors.black45,
borderRadius: BorderRadius.circular(10),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
tipTitle ?? '',
style: const TextStyle(fontSize: 18, color: Colors.white, fontWeight: FontWeight.bold),
),
Text(
tipValue ?? '',
style: const TextStyle(fontSize: 14, color: Colors.white),
),
],
),
),
),
],
),
),
);
}
void _onTapUp(TapUpDetails details) {
final width = contentKey.currentContext.size.width;
final height = contentKey.currentContext.size.height;
final localPosition = details.localPosition;
final transerPosition = Offset(localPosition.dx, height - localPosition.dy);
//是否点击到了点
final finder = rects?.firstWhere((rect) => rect.contains(transerPosition), orElse: () => null);
if (finder != null) {
final index = rects.indexOf(finder);
tipTitle = widget.data[index].title;
tipValue = widget.data[index].value.toString();
setState(() {
isShowTip = true;
autoCloseTip();
});
tipTop = localPosition.dy;
if (localPosition.dx + tipKey.currentContext.size.width > width) {
tipLeft = localPosition.dx - (localPosition.dx + tipKey.currentContext.size.width - width);
} else {
tipLeft = localPosition.dx;
}
setState(() {});
} else {
hideTip();
}
}
//关闭提示计时器
void cancelTipTimer() {
if (tipTimer != null && tipTimer.isActive) {
tipTimer.cancel();
}
}
//自动延迟关闭提示
void autoCloseTip() {
cancelTipTimer();
tipTimer = Timer(Duration(seconds: 3), hideTip);
}
//关闭提示
void hideTip() {
if (isShowTip) {
setState(() {
isShowTip = false;
});
}
}
@override
void didUpdateWidget(covariant HiBarChart oldWidget) {
super.didUpdateWidget(oldWidget);
//更新数据集
if (oldWidget.data != widget.data) {
changeList = Util.compareDiffData(oldWidget.data, widget.data);
changeAnimationController.forward(from: 0);
}
}
@override
void dispose() {
initAnimationController.dispose();
changeAnimationController.dispose();
cancelTipTimer();
super.dispose();
}
}
class _HiBarChartPaint extends CustomPainter {
final List<ChartBean> data;
final BuildContext context;
final Color lineColor;
final lineWidth;
final ValueChanged<List<Rect>> onRectsChanged;
final double translateX;
final int cormax, cormin;
final double progress; //初始化动画进度
final double changeProgress; //数据集变更动画进度
final List<DiffValue> changeList;
Paint _barPaint;
_HiBarChartPaint({
@required this.context,
@required this.data,
this.translateX = 0,
this.onRectsChanged,
this.lineWidth,
this.lineColor,
this.cormin,
this.cormax,
this.progress,
this.changeList,
this.changeProgress,
}) {
init();
}
init() {
_barPaint = Paint()
..color = lineColor ?? Theme.of(context).primaryColor
..strokeWidth = lineWidth + 4
..style = PaintingStyle.fill
..strokeCap = StrokeCap.round;
}
@override
void paint(Canvas canvas, Size size) {
canvas.save();
//调整画布为正常笛卡尔坐标系
canvas.scale(1, -1);
canvas.translate(0, -size.height);
_drawBar(canvas, size);
canvas.restore();
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
//绘制柱形图
void _drawBar(Canvas canvas, Size size) {
canvas.save();
canvas.clipRect(Rect.fromLTWH(constant.pointSpace / 2, 0, size.width - constant.pointSpace / 2, size.height));
List<Rect> rects = [];
for (final item in data) {
final index = data.indexOf(item);
double height = size.height * item.value / (cormax - cormin) * progress;
final finder = changeList?.firstWhere((element) => (element.index == index), orElse: () => null);
if (finder != null) {
final oldHeight = size.height * finder.value / (cormax - cormin) * progress;
height = oldHeight + (height - oldHeight) * changeProgress;
}
Rect rect = Rect.fromLTWH((index + 1) * constant.pointSpace + constant.pointSpace / 2 + translateX, 0, constant.pointSpace / 2, height);
canvas.drawRect(rect, _barPaint);
rects.add(rect);
}
canvas.restore();
if (onRectsChanged != null) {
onRectsChanged(rects);
}
}
}
Copy the code
This class is responsible for drawing the bar chart content area and related click events