Showing
4 changed files
with
432 additions
and
0 deletions
| @@ -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'; |
pdf/lib/widgets/chart.dart
0 → 100644
| 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 | +} |
pdf/test/widget_chart_test.dart
0 → 100644
| 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 | +} |
-
Please register or login to post a comment