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