Marco Papula
Committed by David PHAM-VAN

Add Chart Widget

@@ -6,6 +6,7 @@ @@ -6,6 +6,7 @@
6 - Fix PdfColors.shade() 6 - Fix PdfColors.shade()
7 - Add dashed lines to Decoration Widgets 7 - Add dashed lines to Decoration Widgets
8 - Add TableRow decoration 8 - Add TableRow decoration
  9 +- Add Chart Widget [Marco Papula]
9 10
10 ## 1.6.2 11 ## 1.6.2
11 12
@@ -54,3 +54,4 @@ part 'widgets/text_style.dart'; @@ -54,3 +54,4 @@ part 'widgets/text_style.dart';
54 part 'widgets/theme.dart'; 54 part 'widgets/theme.dart';
55 part 'widgets/widget.dart'; 55 part 'widgets/widget.dart';
56 part 'widgets/wrap.dart'; 56 part 'widgets/wrap.dart';
  57 +part 'widgets/chart.dart';
  1 +/*
  2 + * Copyright (C) 2017, David PHAM-VAN <dev.nfet.net@gmail.com>
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +// ignore_for_file: omit_local_variable_types
  18 +
  19 +part of widget;
  20 +
  21 +bool _isSortedAscending(List<double> list) {
  22 + double prev = list.first;
  23 + for (double elem in list) {
  24 + if (prev > elem) {
  25 + return false;
  26 + }
  27 + prev = elem;
  28 + }
  29 + return true;
  30 +}
  31 +
  32 +class Chart extends Widget {
  33 + Chart({
  34 + @required this.grid,
  35 + @required this.data,
  36 + this.width = 500,
  37 + this.height = 250,
  38 + this.fit = BoxFit.contain,
  39 + });
  40 +
  41 + final double width;
  42 + final double height;
  43 + final BoxFit fit;
  44 + final Grid grid;
  45 + final List<DataSet> data;
  46 + PdfRect gridBox;
  47 +
  48 + @override
  49 + void layout(Context context, BoxConstraints constraints,
  50 + {bool parentUsesSize = false}) {
  51 + final double w = constraints.hasBoundedWidth
  52 + ? constraints.maxWidth
  53 + : constraints.constrainWidth(width.toDouble());
  54 + final double h = constraints.hasBoundedHeight
  55 + ? constraints.maxHeight
  56 + : constraints.constrainHeight(height.toDouble());
  57 +
  58 + final FittedSizes sizes =
  59 + applyBoxFit(fit, PdfPoint(width, height), PdfPoint(w, h));
  60 +
  61 + box = PdfRect.fromPoints(PdfPoint.zero, sizes.destination);
  62 + grid.layout(context, box);
  63 + for (DataSet dataSet in data) {
  64 + dataSet.layout(context, grid.gridBox);
  65 + }
  66 + }
  67 +
  68 + @override
  69 + void paint(Context context) {
  70 + super.paint(context);
  71 +
  72 + final Matrix4 mat = Matrix4.identity();
  73 + mat.translate(box.x, box.y);
  74 + context.canvas
  75 + ..saveContext()
  76 + ..setTransform(mat);
  77 +
  78 + grid.paint(context, box);
  79 + for (DataSet dataSet in data) {
  80 + dataSet.paint(context, grid);
  81 + }
  82 + context.canvas.restoreContext();
  83 + }
  84 +}
  85 +
  86 +abstract class Grid {
  87 + PdfRect gridBox;
  88 + double xOffset;
  89 + double xTotal;
  90 + double yOffset;
  91 + double yTotal;
  92 +
  93 + void layout(Context context, PdfRect box);
  94 + void paint(Context context, PdfRect box);
  95 +}
  96 +
  97 +class LinearGrid extends Grid {
  98 + LinearGrid({
  99 + @required this.xAxis,
  100 + @required this.yAxis,
  101 + this.xMargin = 10,
  102 + this.yMargin = 2,
  103 + this.textStyle,
  104 + this.lineWidth = 1,
  105 + this.color = PdfColors.black,
  106 + this.separatorLineWidth = 1,
  107 + this.separatorColor = PdfColors.grey,
  108 + }) : assert(_isSortedAscending(xAxis)),
  109 + assert(_isSortedAscending(yAxis));
  110 +
  111 + final List<double> xAxis;
  112 + final List<double> yAxis;
  113 + final double xMargin;
  114 + final double yMargin;
  115 + final TextStyle textStyle;
  116 + final double lineWidth;
  117 + final PdfColor color;
  118 +
  119 + TextStyle style;
  120 + PdfFont font;
  121 + PdfFontMetrics xAxisFontMetric;
  122 + PdfFontMetrics yAxisFontMetric;
  123 + double separatorLineWidth;
  124 + PdfColor separatorColor;
  125 +
  126 + @override
  127 + void layout(Context context, PdfRect box) {
  128 + style = Theme.of(context).defaultTextStyle.merge(textStyle);
  129 + font = style.font.getFont(context);
  130 +
  131 + xAxisFontMetric =
  132 + font.stringMetrics(xAxis.reduce(math.max).toStringAsFixed(1)) *
  133 + (style.fontSize);
  134 + yAxisFontMetric =
  135 + font.stringMetrics(yAxis.reduce(math.max).toStringAsFixed(1)) *
  136 + (style.fontSize);
  137 +
  138 + gridBox = PdfRect.fromLTRB(
  139 + box.left + yAxisFontMetric.width + xMargin,
  140 + box.bottom + xAxisFontMetric.height + yMargin,
  141 + box.right - xAxisFontMetric.width / 2,
  142 + box.top - yAxisFontMetric.height / 2);
  143 +
  144 + xOffset = xAxis.reduce(math.min);
  145 + yOffset = yAxis.reduce(math.min);
  146 + xTotal = xAxis.reduce(math.max) - xOffset;
  147 + yTotal = yAxis.reduce(math.max) - yOffset;
  148 + }
  149 +
  150 + @override
  151 + void paint(Context context, PdfRect box) {
  152 + xAxis.asMap().forEach((int i, double x) {
  153 + context.canvas
  154 + ..setColor(style.color)
  155 + ..drawString(
  156 + style.font.getFont(context),
  157 + style.fontSize,
  158 + x.toStringAsFixed(1),
  159 + gridBox.left +
  160 + gridBox.width * i / (xAxis.length - 1) -
  161 + xAxisFontMetric.width / 2,
  162 + 0,
  163 + );
  164 + });
  165 +
  166 + for (double y in yAxis.where((double y) => y != yAxis.first)) {
  167 + final double textWidth =
  168 + (font.stringMetrics(y.toStringAsFixed(1)) * (style.fontSize)).width;
  169 + final double yPos = gridBox.bottom + gridBox.height * y / yAxis.last;
  170 + context.canvas
  171 + ..setColor(style.color)
  172 + ..drawString(
  173 + style.font.getFont(context),
  174 + style.fontSize,
  175 + y.toStringAsFixed(1),
  176 + xAxisFontMetric.width / 2 - textWidth / 2,
  177 + yPos - font.ascent,
  178 + );
  179 +
  180 + context.canvas.drawLine(
  181 + gridBox.left,
  182 + yPos + font.descent + font.ascent - separatorLineWidth / 2,
  183 + gridBox.right,
  184 + yPos + font.descent + font.ascent - separatorLineWidth / 2);
  185 + }
  186 + context.canvas
  187 + ..setStrokeColor(separatorColor)
  188 + ..setLineWidth(separatorLineWidth)
  189 + ..strokePath();
  190 +
  191 + context.canvas
  192 + ..setStrokeColor(color)
  193 + ..setLineWidth(lineWidth)
  194 + ..drawLine(gridBox.left, gridBox.bottom, gridBox.right, gridBox.bottom)
  195 + ..drawLine(gridBox.left, gridBox.bottom, gridBox.left, gridBox.top)
  196 + ..strokePath();
  197 + }
  198 +}
  199 +
  200 +class ChartValue {
  201 + ChartValue(this.x, this.y);
  202 + final double x;
  203 + final double y;
  204 +}
  205 +
  206 +abstract class DataSet {
  207 + void layout(Context context, PdfRect box);
  208 + void paint(Context context, Grid grid);
  209 +}
  210 +
  211 +class LineDataSet extends DataSet {
  212 + LineDataSet({
  213 + @required this.data,
  214 + this.pointColor = PdfColors.green,
  215 + this.pointSize = 8,
  216 + this.lineColor = PdfColors.blue,
  217 + this.lineWidth = 2,
  218 + this.drawLine = true,
  219 + this.drawPoints = true,
  220 + this.lineStartingPoint,
  221 + }) : assert(drawLine || drawPoints);
  222 +
  223 + final List<ChartValue> data;
  224 + final PdfColor pointColor;
  225 + final double pointSize;
  226 + final PdfColor lineColor;
  227 + final double lineWidth;
  228 + final bool drawLine;
  229 + final bool drawPoints;
  230 + final ChartValue lineStartingPoint;
  231 +
  232 + double maxValue;
  233 +
  234 + @override
  235 + void layout(Context context, PdfRect box) {}
  236 +
  237 + @override
  238 + void paint(Context context, Grid grid) {
  239 + if (drawLine) {
  240 + ChartValue lastValue = lineStartingPoint;
  241 + for (ChartValue value in data) {
  242 + if (lastValue != null) {
  243 + context.canvas.drawLine(
  244 + grid.gridBox.left +
  245 + grid.gridBox.width * (lastValue.x - grid.xOffset) / grid.xTotal,
  246 + grid.gridBox.bottom +
  247 + grid.gridBox.height *
  248 + (lastValue.y - grid.yOffset) /
  249 + grid.yTotal,
  250 + grid.gridBox.left +
  251 + grid.gridBox.width * (value.x - grid.xOffset) / grid.xTotal,
  252 + grid.gridBox.bottom +
  253 + grid.gridBox.height * (value.y - grid.yOffset) / grid.yTotal,
  254 + );
  255 + }
  256 + lastValue = value;
  257 + }
  258 +
  259 + context.canvas
  260 + ..setStrokeColor(lineColor)
  261 + ..setLineWidth(lineWidth)
  262 + ..setLineCap(PdfLineCap.joinRound)
  263 + ..setLineJoin(PdfLineCap.joinRound)
  264 + ..strokePath();
  265 + }
  266 +
  267 + if (drawPoints) {
  268 + for (ChartValue value in data) {
  269 + context.canvas
  270 + ..setColor(pointColor)
  271 + ..drawEllipse(
  272 + grid.gridBox.left +
  273 + grid.gridBox.width * (value.x - grid.xOffset) / grid.xTotal,
  274 + grid.gridBox.bottom +
  275 + grid.gridBox.height * (value.y - grid.yOffset) / grid.yTotal,
  276 + pointSize,
  277 + pointSize)
  278 + ..fillPath();
  279 + }
  280 + }
  281 + }
  282 +}
  1 +/*
  2 + * Copyright (C) 2017, David PHAM-VAN <dev.nfet.net@gmail.com>
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +// ignore_for_file: omit_local_variable_types
  18 +
  19 +import 'dart:io';
  20 +
  21 +import 'package:test/test.dart';
  22 +import 'package:pdf/pdf.dart';
  23 +import 'package:pdf/widgets.dart';
  24 +
  25 +Document pdf;
  26 +
  27 +void main() {
  28 + setUpAll(() {
  29 + Document.debug = true;
  30 + pdf = Document();
  31 + });
  32 +
  33 + group('LineChart test', () {
  34 + test('Default LineChart', () {
  35 + pdf.addPage(Page(
  36 + build: (Context context) => Chart(
  37 + grid: LinearGrid(
  38 + xAxis: <double>[0, 1, 2, 3, 4, 5, 6],
  39 + yAxis: <double>[0, 3, 6, 9],
  40 + ),
  41 + data: <DataSet>[
  42 + LineDataSet(
  43 + data: <ChartValue>[
  44 + ChartValue(1, 1),
  45 + ChartValue(2, 3),
  46 + ChartValue(3, 7),
  47 + ],
  48 + ),
  49 + ],
  50 + ),
  51 + ));
  52 + });
  53 +
  54 + test('Default LineChart without lines connecting points', () {
  55 + pdf.addPage(Page(
  56 + build: (Context context) => Chart(
  57 + grid: LinearGrid(
  58 + xAxis: <double>[0, 1, 2, 3, 4, 5, 6],
  59 + yAxis: <double>[0, 3, 6, 9],
  60 + ),
  61 + data: <DataSet>[
  62 + LineDataSet(
  63 + data: <ChartValue>[
  64 + ChartValue(1, 1),
  65 + ChartValue(2, 3),
  66 + ChartValue(3, 7),
  67 + ],
  68 + drawLine: false,
  69 + ),
  70 + ],
  71 + ),
  72 + ));
  73 + });
  74 +
  75 + test('Default ScatterChart without dots', () {
  76 + pdf.addPage(Page(
  77 + build: (Context context) => Chart(
  78 + grid: LinearGrid(
  79 + xAxis: <double>[0, 1, 2, 3, 4, 5, 6],
  80 + yAxis: <double>[0, 3, 6, 9],
  81 + ),
  82 + data: <DataSet>[
  83 + LineDataSet(
  84 + data: <ChartValue>[
  85 + ChartValue(1, 1),
  86 + ChartValue(2, 3),
  87 + ChartValue(3, 7),
  88 + ],
  89 + drawPoints: false,
  90 + ),
  91 + ],
  92 + ),
  93 + ));
  94 + });
  95 +
  96 + test('ScatterChart with custom points and lines', () {
  97 + pdf.addPage(Page(
  98 + build: (Context context) => Chart(
  99 + grid: LinearGrid(
  100 + xAxis: <double>[0, 1, 2, 3, 4, 5, 6],
  101 + yAxis: <double>[0, 3, 6, 9],
  102 + ),
  103 + data: <DataSet>[
  104 + LineDataSet(
  105 + data: <ChartValue>[
  106 + ChartValue(1, 1),
  107 + ChartValue(2, 3),
  108 + ChartValue(3, 7),
  109 + ],
  110 + drawLine: false,
  111 + pointColor: PdfColors.red,
  112 + pointSize: 4,
  113 + lineColor: PdfColors.purple,
  114 + lineWidth: 4,
  115 + ),
  116 + ],
  117 + ),
  118 + ));
  119 + });
  120 +
  121 + test('ScatterChart with custom size', () {
  122 + pdf.addPage(Page(
  123 + build: (Context context) => Chart(
  124 + width: 200,
  125 + height: 100,
  126 + grid: LinearGrid(
  127 + xAxis: <double>[0, 1, 2, 3, 4, 5, 6],
  128 + yAxis: <double>[0, 3, 6, 9],
  129 + ),
  130 + data: <DataSet>[
  131 + LineDataSet(
  132 + data: <ChartValue>[
  133 + ChartValue(1, 1),
  134 + ChartValue(2, 3),
  135 + ChartValue(3, 7),
  136 + ],
  137 + ),
  138 + ],
  139 + ),
  140 + ));
  141 + });
  142 + });
  143 +
  144 + tearDownAll(() {
  145 + final File file = File('widgets-chart.pdf');
  146 + file.writeAsBytesSync(pdf.save());
  147 + });
  148 +}