🐊
【Flutter】CustomPaintで折れ線グラフを作成する
今回やること
今回は CustomPaint を使って折れ線グラフを作っていきます。
CustomPaint で作成するので自由にカスタムも可能です。
まずはエリア分け
まずは解説のために、画像のようにエリア分けておきます。
それぞれ描画していく
CustomPainterのコンストラクタ等
List<String> labels = ['12', '13', '14', '15', '16', '17', '18'];
List<double> values = [13, 23, 23, 20, 32, 15, 30];
double gridInterval =10.0;
maxValue = ((_values.reduce(math.max) ~/ gridInterval) + 1) * gridInterval;
minValue = (_values.reduce(math.min) ~/ gridInterval) * gridInterval;
class LineChartPainter extends CustomPainter {
final List<String> labels; // 下のラベル
final List<double> values; //各ラベルの値
final double maxValue; // _valuesの最大値の切り上げたインターバル値
final double minValue; // _valuesの最小値の切り下げたインターバル値
final double gridInterval;
LineChartPainter({
required this.labels,
required this.values,
required this.maxValue,
required this.minValue,
required this.gridInterval,
});
static const double upperAreaHeight = 50;
static const double lowerAreaHeight = 50;
static const double leftAreaWidth = 50;
static const double rightAreaWidth = 30;
static Color bgColor = Colors.grey.shade900;
static Color lineColor = Colors.green.shade500;
static Color baseLineColor = Colors.green.shade300;
static Color gridColor = Colors.green.shade100;
static Color textColor = Colors.grey;
1. 描画範囲を指定して背景を描画
void paint(Canvas canvas, Size size) {
final clipRect = Rect.fromLTWH(0, 0, size.width, size.height);
canvas.clipRect(clipRect); // 描画領域を制限
canvas.drawPaint(Paint()..color = bgColor); // 背景描画
...
2. 各値の点を作成
final valueAreaHeight = size.height - upperAreaHeight - lowerAreaHeight;
final valueAreaWidth = (size.width - leftAreaWidth - rightAreaWidth) / labels.length;
final points = _createPoint(valueAreaHeight, valueAreaWidth);
List<Offset> _createPoint(double valueAreaHeight, double valueAreaWidth) {
final List<Offset> points = [];
for (int i = 0; i < labels.length; i++) {
final x = leftAreaWidth + valueAreaWidth * i + valueAreaWidth / 2;
final y = upperAreaHeight + valueAreaHeight - (valueAreaHeight / (maxValue - minValue) * (values[i] - minValue));
points.add(Offset(x, y));
}
return points;
}
3. 作成した点をもとに折れ線を描画
void _drawLine(Canvas canvas, List<Offset> points) {
final Paint paint = Paint()
..color = lineColor
..style = PaintingStyle.stroke
..strokeWidth = 2;
final path = Path();
for (int i = 0; i < points.length; i++) {
if (i == 0) {
path.moveTo(points[i].dx, points[i].dy); //始点へ移動
} else {
path.lineTo(points[i].dx, points[i].dy); //次点へ線を引く
}
canvas.drawPath(path, paint);
}
}
/// 点に丸を描画
void _drawPoint(Canvas canvas, List<Offset> points) {
final fillPaint = Paint()
..color = bgColor
..style = PaintingStyle.fill; // 塗りつぶし
final strokePaint = Paint()
..color = lineColor
..style = PaintingStyle.stroke
..strokeWidth = 2;
for (final point in points) {
canvas.drawCircle(point, 5, fillPaint);
canvas.drawCircle(point, 5, strokePaint);
}
}
4. ベース線とグリット線を描画
/// leftAreaのラベル(高さはグリッド線と同じ)
class LeftLabel {
final String label;
final double height;
LeftLabel(this.label, this.height);
}
final baseLineHeight = size.height - lowerAreaHeight;
List<LeftLabel> _intervalHeightList(double valueAreaHeight, double baseLineHeight) {
final List<LeftLabel> intervalHeightList = [];
final intervalCount = (maxValue ~/ gridInterval) - (minValue ~/ gridInterval);
for (int i = 0; i < intervalCount; i++) {
final labelValue = minValue + gridInterval * (i + 1);
final height = baseLineHeight - valueAreaHeight / (maxValue - minValue) * (labelValue - minValue);
intervalHeightList.add(LeftLabel(labelValue.toString(), height));
}
return intervalHeightList;
}
void _drawBaseLine(Canvas canvas, Size size, double baseLineHeight) {
final Paint paint = Paint()
..color = baseLineColor
..style = PaintingStyle.stroke
..strokeWidth = 2;
final path = Path()
..moveTo(leftAreaWidth, baseLineHeight)
..lineTo(size.width - rightAreaWidth, baseLineHeight);
canvas.drawPath(path, paint);
}
void _drawIntervalLine(Canvas canvas, Size size, double valueAreaHeight, double baseLineHeight) {
const dashWidth = 5.0;
const dashSpace = 3.0;
final Paint paint = Paint()
..color = gridColor
..style = PaintingStyle.stroke
..strokeWidth = 1;
final List<LeftLabel> intervalList = _intervalHeightList(valueAreaHeight, baseLineHeight);
for (int i = 0; i < intervalList.length; i++) {
final path = Path();
double startX = leftAreaWidth;
/// 破線の描画
while (startX < size.width - rightAreaWidth) {
path.moveTo(startX, intervalList[i].height);
startX += dashWidth;
path.lineTo(startX, intervalList[i].height);
startX += dashSpace;
}
canvas.drawPath(path, paint);
}
}
5. ラベルテキストの描画
テキストの描画は TextPainter()を使います。
/// lowerArea のラベル
void _drawLowerLabelTextPainter(Canvas canvas, Size size) {
const paddingTop = 10;
for (int i = 0; i < labels.length; i++) {
final textPainter = TextPainter(
text: TextSpan(
text: labels[i],
style: TextStyle(color: textColor, fontSize: 20),
),
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(
leftAreaWidth +
(i + 0.5) * (size.width - leftAreaWidth - rightAreaWidth) / labels.length -
textPainter.width / 2,
size.height - lowerAreaHeight + paddingTop));
}
}
/// leftArea のラベル
void _drawLeftLabelTextPainter(Canvas canvas, Size size, double valueAreaHeight, double baseLineHeight) {
final intervalList = _intervalHeightList(valueAreaHeight, baseLineHeight);
const paddingRight = 10;
for (int i = 0; i < intervalList.length; i++) {
final textPainter = TextPainter(
text: TextSpan(
text: intervalList[i].label.split('.')[0],
style: TextStyle(color: textColor, fontSize: 20),
),
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(
leftAreaWidth - textPainter.width - paddingRight,
intervalList[i].height - textPainter.height / 2,
));
}
}
終わりに
少しコードが多めで見にくくなってしまいましたが、
CustomPaint を使ってグラフを作る際には参考にしていただければと思います。
コード全文
全文
import 'package:flutter/material.dart';
import 'dart:math' as math;
class LineChartPage extends StatefulWidget {
const LineChartPage({super.key});
State<LineChartPage> createState() => _LineChartPageState();
}
class _LineChartPageState extends State<LineChartPage> {
static const List<String> _labels = ['12', '13', '14', '15', '16', '17', '18']; // 下のラベル
static const intervalValueList = [5.0, 10.0, 20.0]; // グリッドの間隔の種類
List<double> _values = [];
double gridInterval = 10.0;
double _maxValue = 0.0; // _valuesの最大値の切り上げたインターバル値
double _minValue = 0.0; // _valuesの最小値の切り下げたインターバル値
void initValue() {
_values = [13, 23, 23, 20, 32, 15, 30];
_maxValue = ((_values.reduce(math.max) ~/ gridInterval) + 1) * gridInterval;
_minValue = (_values.reduce(math.min) ~/ gridInterval) * gridInterval;
}
void shuffleValue() {
final random = math.Random();
for (int i = 0; i < _values.length; i++) {
_values[i] = random.nextInt(50).toDouble();
}
setState(() {
_maxValue = ((_values.reduce(math.max) ~/ gridInterval) + 1) * gridInterval;
_minValue = (_values.reduce(math.min) ~/ gridInterval) * gridInterval;
});
}
void initState() {
super.initState();
initValue();
}
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: [
SizedBox(
width: MediaQuery.sizeOf(context).width,
height: 300,
child: CustomPaint(
painter: LineChartPainter(
labels: _labels,
values: _values,
maxValue: _maxValue,
minValue: _minValue,
gridInterval: gridInterval),
),
),
const SizedBox(height: 20),
Text(
'values : ${_values.map((e) => e.toStringAsFixed(0)).join(', ')}',
style: const TextStyle(fontSize: 20),
),
const SizedBox(height: 10),
ElevatedButton(
onPressed: () {
shuffleValue();
},
child: const Text('Shuffle')),
const SizedBox(height: 5),
SizedBox(
width: MediaQuery.sizeOf(context).width,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: intervalValueList.map((interval) {
return Expanded(
child: ListTile(
title: Text(interval.toStringAsFixed(0)),
leading: Radio<double>(
value: interval.toDouble(),
groupValue: gridInterval,
onChanged: (double? value) {
setState(() {
gridInterval = value!;
});
},
),
),
);
}).toList(),
),
)
],
),
));
}
}
class LineChartPainter extends CustomPainter {
final List<String> labels; // 下のラベル
final List<double> values; //各ラベルの値
final double maxValue; // _valuesの最大値の切り上げたインターバル値
final double minValue; // _valuesの最小値の切り下げたインターバル値
final double gridInterval;
LineChartPainter({
required this.labels,
required this.values,
required this.maxValue,
required this.minValue,
required this.gridInterval,
});
static const double upperAreaHeight = 50;
static const double lowerAreaHeight = 50;
static const double leftAreaWidth = 50;
static const double rightAreaWidth = 30;
static Color bgColor = Colors.grey.shade900;
static Color lineColor = Colors.green.shade500;
static Color baseLineColor = Colors.green.shade300;
static Color gridColor = Colors.green.shade100;
static Color textColor = Colors.grey;
void paint(Canvas canvas, Size size) {
final clipRect = Rect.fromLTWH(0, 0, size.width, size.height);
/// 背景描画
canvas.clipRect(clipRect); // 描画領域を制限
canvas.drawPaint(Paint()..color = bgColor);
final valueAreaHeight = size.height - upperAreaHeight - lowerAreaHeight;
final valueAreaWidth = (size.width - leftAreaWidth - rightAreaWidth) / labels.length;
final baseLineHeight = size.height - lowerAreaHeight;
final points = _createPoint(valueAreaHeight, valueAreaWidth);
_drawLowerLabelTextPainter(canvas, size); // lowerArea
_drawLeftLabelTextPainter(canvas, size, valueAreaHeight, baseLineHeight); // leftArea
_drawBaseLine(canvas, size, baseLineHeight);
_drawIntervalLine(canvas, size, valueAreaHeight, baseLineHeight);
_drawLine(canvas, points);
_drawPoint(canvas, points);
}
List<Offset> _createPoint(double valueAreaHeight, double valueAreaWidth) {
final List<Offset> points = [];
for (int i = 0; i < labels.length; i++) {
final x = leftAreaWidth + valueAreaWidth * (i) + valueAreaWidth / 2;
final y = upperAreaHeight + valueAreaHeight - (valueAreaHeight / (maxValue - minValue) * (values[i] - minValue));
points.add(Offset(x, y));
}
return points;
}
List<LeftLabel> _intervalHeightList(double valueAreaHeight, double baseLineHeight) {
final List<LeftLabel> intervalHeightList = [];
final intervalCount = (maxValue ~/ gridInterval) - (minValue ~/ gridInterval);
for (int i = 0; i < intervalCount; i++) {
final labelValue = minValue + gridInterval * (i + 1);
final height = baseLineHeight - valueAreaHeight / (maxValue - minValue) * (labelValue - minValue);
intervalHeightList.add(LeftLabel(labelValue.toString(), height));
}
return intervalHeightList;
}
void _drawLine(Canvas canvas, List<Offset> points) {
final Paint paint = Paint()
..color = lineColor
..style = PaintingStyle.stroke
..strokeWidth = 2;
final path = Path();
for (int i = 0; i < points.length; i++) {
if (i == 0) {
path.moveTo(points[i].dx, points[i].dy);
} else {
path.lineTo(points[i].dx, points[i].dy);
}
canvas.drawPath(path, paint);
}
}
void _drawPoint(Canvas canvas, List<Offset> points) {
final fillPaint = Paint()
..color = bgColor
..style = PaintingStyle.fill;
final strokePaint = Paint()
..color = lineColor
..style = PaintingStyle.stroke
..strokeWidth = 2;
for (final point in points) {
canvas.drawCircle(point, 5, fillPaint);
canvas.drawCircle(point, 5, strokePaint);
}
}
void _drawBaseLine(Canvas canvas, Size size, double baseLineHeight) {
final Paint paint = Paint()
..color = baseLineColor
..style = PaintingStyle.stroke
..strokeWidth = 2;
final path = Path()
..moveTo(leftAreaWidth, baseLineHeight)
..lineTo(size.width - rightAreaWidth, baseLineHeight);
canvas.drawPath(path, paint);
}
void _drawIntervalLine(Canvas canvas, Size size, double valueAreaHeight, double baseLineHeight) {
const dashWidth = 5.0;
const dashSpace = 3.0;
final Paint paint = Paint()
..color = gridColor
..style = PaintingStyle.stroke
..strokeWidth = 1;
final List<LeftLabel> intervalList = _intervalHeightList(valueAreaHeight, baseLineHeight);
for (int i = 0; i < intervalList.length; i++) {
final path = Path();
double startX = leftAreaWidth;
/// 破線の描画
while (startX < size.width - rightAreaWidth) {
path.moveTo(startX, intervalList[i].height);
startX += dashWidth;
path.lineTo(startX, intervalList[i].height);
startX += dashSpace;
}
canvas.drawPath(path, paint);
}
}
void _drawLowerLabelTextPainter(Canvas canvas, Size size) {
const paddingTop = 10;
for (int i = 0; i < labels.length; i++) {
final textPainter = TextPainter(
text: TextSpan(
text: labels[i],
style: TextStyle(color: textColor, fontSize: 20),
),
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(
leftAreaWidth +
(i + 0.5) * (size.width - leftAreaWidth - rightAreaWidth) / labels.length -
textPainter.width / 2,
size.height - lowerAreaHeight + paddingTop));
}
}
void _drawLeftLabelTextPainter(Canvas canvas, Size size, double valueAreaHeight, double baseLineHeight) {
final intervalList = _intervalHeightList(valueAreaHeight, baseLineHeight);
const paddingRight = 10;
for (int i = 0; i < intervalList.length; i++) {
final textPainter = TextPainter(
text: TextSpan(
text: intervalList[i].label.split('.')[0],
style: TextStyle(color: textColor, fontSize: 20),
),
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(
leftAreaWidth - textPainter.width - paddingRight,
intervalList[i].height - textPainter.height / 2,
));
}
}
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
class LeftLabel {
final String label;
final double height;
LeftLabel(this.label, this.height);
}
Discussion