David PHAM-VAN

Add PieChart

1 include: package:pedantic/analysis_options.yaml 1 include: package:pedantic/analysis_options.yaml
2 -  
3 -analyzer:  
4 - strong-mode:  
5 - implicit-dynamic: false  
6 - errors:  
7 - missing_required_param: warning  
8 - missing_return: warning  
9 -  
10 -linter:  
11 - rules:  
12 - - always_put_control_body_on_new_line  
13 - - avoid_as  
14 - - avoid_bool_literals_in_conditional_expressions  
15 - - avoid_classes_with_only_static_members  
16 - - avoid_field_initializers_in_const_classes  
17 - - avoid_function_literals_in_foreach_calls  
18 - - avoid_renaming_method_parameters  
19 - - avoid_returning_null_for_void  
20 - - avoid_slow_async_io  
21 - - avoid_unused_constructor_parameters  
22 - - avoid_void_async  
23 - - await_only_futures  
24 - - camel_case_types  
25 - - cancel_subscriptions  
26 - - control_flow_in_finally  
27 - - directives_ordering  
28 - - empty_statements  
29 - - flutter_style_todos  
30 - - hash_and_equals  
31 - - implementation_imports  
32 - - iterable_contains_unrelated_type  
33 - - list_remove_unrelated_type  
34 - - no_adjacent_strings_in_list  
35 - - non_constant_identifier_names  
36 - - omit_local_variable_types  
37 - - overridden_fields  
38 - - package_api_docs  
39 - - package_names  
40 - - package_prefixed_library_names  
41 - - prefer_asserts_in_initializer_lists  
42 - - prefer_const_constructors  
43 - - prefer_const_constructors_in_immutables  
44 - - prefer_const_declarations  
45 - - prefer_const_literals_to_create_immutables  
46 - - prefer_final_locals  
47 - - prefer_foreach  
48 - - prefer_if_elements_to_conditional_expressions  
49 - - prefer_initializing_formals  
50 - - prefer_inlined_adds  
51 - - prefer_typing_uninitialized_variables  
52 - - prefer_void_to_null  
53 - - sort_constructors_first  
54 - - sort_pub_dependencies  
55 - - sort_unnamed_constructors_first  
56 - - test_types_in_equals  
57 - - throw_in_finally  
58 - - unnecessary_brace_in_string_interps  
59 - - unnecessary_getters_setters  
60 - - unnecessary_null_aware_assignments  
61 - - unnecessary_overrides  
62 - - unnecessary_parenthesis  
63 - - unnecessary_statements  
64 - - use_full_hex_values_for_flutter_colors  
@@ -25,29 +25,40 @@ Future<Uint8List> generateReport(PdfPageFormat pageFormat) async { @@ -25,29 +25,40 @@ Future<Uint8List> generateReport(PdfPageFormat pageFormat) async {
25 const tableHeaders = ['Category', 'Budget', 'Expense', 'Result']; 25 const tableHeaders = ['Category', 'Budget', 'Expense', 'Result'];
26 26
27 const dataTable = [ 27 const dataTable = [
28 - ['Phone', 80, 95, -15],  
29 - ['Internet', 250, 230, 20],  
30 - ['Electricity', 300, 375, -75],  
31 - ['Movies', 85, 80, 5],  
32 - ['Food', 300, 350, -50],  
33 - ['Fuel', 650, 550, 100],  
34 - ['Insurance', 250, 310, -60], 28 + ['Phone', 80, 95],
  29 + ['Internet', 250, 230],
  30 + ['Electricity', 300, 375],
  31 + ['Movies', 85, 80],
  32 + ['Food', 300, 350],
  33 + ['Fuel', 650, 550],
  34 + ['Insurance', 250, 310],
35 ]; 35 ];
36 36
  37 + // Some summary maths
  38 + final budget = dataTable
  39 + .map((e) => e[1] as num)
  40 + .reduce((value, element) => value + element);
  41 + final expense = dataTable
  42 + .map((e) => e[2] as num)
  43 + .reduce((value, element) => value + element);
  44 +
37 final baseColor = PdfColors.cyan; 45 final baseColor = PdfColors.cyan;
38 46
39 // Create a PDF document. 47 // Create a PDF document.
40 final document = pw.Document(); 48 final document = pw.Document();
41 49
  50 + final theme = pw.ThemeData.withFont(
  51 + base: pw.Font.ttf(await rootBundle.load('assets/open-sans.ttf')),
  52 + bold: pw.Font.ttf(await rootBundle.load('assets/open-sans-bold.ttf')),
  53 + );
  54 +
42 // Add page to the PDF 55 // Add page to the PDF
43 document.addPage( 56 document.addPage(
44 pw.Page( 57 pw.Page(
45 pageFormat: pageFormat, 58 pageFormat: pageFormat,
46 - theme: pw.ThemeData.withFont(  
47 - base: pw.Font.ttf(await rootBundle.load('assets/open-sans.ttf')),  
48 - bold: pw.Font.ttf(await rootBundle.load('assets/open-sans-bold.ttf')),  
49 - ), 59 + theme: theme,
50 build: (context) { 60 build: (context) {
  61 + // Top bar chart
51 final chart1 = pw.Chart( 62 final chart1 = pw.Chart(
52 left: pw.Container( 63 left: pw.Container(
53 alignment: pw.Alignment.topCenter, 64 alignment: pw.Alignment.topCenter,
@@ -117,6 +128,7 @@ Future<Uint8List> generateReport(PdfPageFormat pageFormat) async { @@ -117,6 +128,7 @@ Future<Uint8List> generateReport(PdfPageFormat pageFormat) async {
117 ], 128 ],
118 ); 129 );
119 130
  131 + // Left curved line chart
120 final chart2 = pw.Chart( 132 final chart2 = pw.Chart(
121 grid: pw.CartesianGrid( 133 grid: pw.CartesianGrid(
122 xAxis: pw.FixedAxis([0, 1, 2, 3, 4, 5, 6]), 134 xAxis: pw.FixedAxis([0, 1, 2, 3, 4, 5, 6]),
@@ -143,10 +155,19 @@ Future<Uint8List> generateReport(PdfPageFormat pageFormat) async { @@ -143,10 +155,19 @@ Future<Uint8List> generateReport(PdfPageFormat pageFormat) async {
143 ], 155 ],
144 ); 156 );
145 157
  158 + // Data table
146 final table = pw.Table.fromTextArray( 159 final table = pw.Table.fromTextArray(
147 border: null, 160 border: null,
148 headers: tableHeaders, 161 headers: tableHeaders,
149 - data: dataTable, 162 + data: List<List<dynamic>>.generate(
  163 + dataTable.length,
  164 + (index) => <dynamic>[
  165 + dataTable[index][0],
  166 + dataTable[index][1],
  167 + dataTable[index][2],
  168 + (dataTable[index][1] as num) - (dataTable[index][2] as num),
  169 + ],
  170 + ),
150 headerStyle: pw.TextStyle( 171 headerStyle: pw.TextStyle(
151 color: PdfColors.white, 172 color: PdfColors.white,
152 fontWeight: pw.FontWeight.bold, 173 fontWeight: pw.FontWeight.bold,
@@ -162,8 +183,11 @@ Future<Uint8List> generateReport(PdfPageFormat pageFormat) async { @@ -162,8 +183,11 @@ Future<Uint8List> generateReport(PdfPageFormat pageFormat) async {
162 ), 183 ),
163 ), 184 ),
164 ), 185 ),
  186 + cellAlignment: pw.Alignment.centerRight,
  187 + cellAlignments: {0: pw.Alignment.centerLeft},
165 ); 188 );
166 189
  190 + // Page layout
167 return pw.Column( 191 return pw.Column(
168 children: [ 192 children: [
169 pw.Text('Budget Report', 193 pw.Text('Budget Report',
@@ -223,7 +247,7 @@ Future<Uint8List> generateReport(PdfPageFormat pageFormat) async { @@ -223,7 +247,7 @@ Future<Uint8List> generateReport(PdfPageFormat pageFormat) async {
223 ), 247 ),
224 ), 248 ),
225 pw.Text( 249 pw.Text(
226 - 'Budget was originally \$1915. A total of \$1990 was spent on the month of January which exceeded the overall budget by \$75', 250 + 'Budget was originally \$$budget. A total of \$$expense was spent on the month of January which exceeded the overall budget by \$${expense - budget}',
227 textAlign: pw.TextAlign.justify, 251 textAlign: pw.TextAlign.justify,
228 ) 252 )
229 ], 253 ],
@@ -237,6 +261,55 @@ Future<Uint8List> generateReport(PdfPageFormat pageFormat) async { @@ -237,6 +261,55 @@ Future<Uint8List> generateReport(PdfPageFormat pageFormat) async {
237 ), 261 ),
238 ); 262 );
239 263
  264 + // Second page with a pie chart
  265 + document.addPage(
  266 + pw.Page(
  267 + pageFormat: pageFormat,
  268 + theme: theme,
  269 + build: (context) {
  270 + const chartColors = [
  271 + PdfColors.blue300,
  272 + PdfColors.green300,
  273 + PdfColors.amber300,
  274 + PdfColors.pink300,
  275 + PdfColors.cyan300,
  276 + PdfColors.purple300,
  277 + PdfColors.lime300,
  278 + ];
  279 +
  280 + return pw.SizedBox(
  281 + height: 400,
  282 + child: pw.Chart(
  283 + title: pw.Text(
  284 + 'Expense breakdown',
  285 + style: pw.TextStyle(
  286 + color: baseColor,
  287 + fontSize: 20,
  288 + ),
  289 + ),
  290 + grid: pw.PieGrid(),
  291 + datasets: List<pw.Dataset>.generate(dataTable.length, (index) {
  292 + final data = dataTable[index];
  293 + final color = chartColors[index % chartColors.length];
  294 + final textColor =
  295 + color.luminance < 0.2 ? PdfColors.white : PdfColors.black;
  296 +
  297 + final value = (data[2] as num).toDouble();
  298 + final pct = (value / expense * 100).round();
  299 +
  300 + return pw.PieDataSet(
  301 + legend: '${data[0]}\n$pct%',
  302 + value: value,
  303 + color: color,
  304 + legendStyle: pw.TextStyle(fontSize: 10, color: textColor),
  305 + );
  306 + }),
  307 + ),
  308 + );
  309 + },
  310 + ),
  311 + );
  312 +
240 // Return the PDF file content 313 // Return the PDF file content
241 return document.save(); 314 return document.save();
242 } 315 }
1 # Changelog 1 # Changelog
2 2
3 -## 3.0.2 3 +## 3.1.0
4 4
5 - Fix some linting issues 5 - Fix some linting issues
6 - Add PdfPage.rotate attribute 6 - Add PdfPage.rotate attribute
7 - Add RadialGrid for charts with polar coordinates 7 - Add RadialGrid for charts with polar coordinates
  8 +- Add PieChart
8 9
9 ## 3.0.1 10 ## 3.0.1
10 11
@@ -35,6 +35,7 @@ class ChartLegend extends StatelessWidget { @@ -35,6 +35,7 @@ class ChartLegend extends StatelessWidget {
35 this.direction = Axis.vertical, 35 this.direction = Axis.vertical,
36 this.decoration, 36 this.decoration,
37 this.padding = const EdgeInsets.all(5), 37 this.padding = const EdgeInsets.all(5),
  38 + this.maxWidth = 200,
38 }); 39 });
39 40
40 final TextStyle? textStyle; 41 final TextStyle? textStyle;
@@ -47,6 +48,8 @@ class ChartLegend extends StatelessWidget { @@ -47,6 +48,8 @@ class ChartLegend extends StatelessWidget {
47 48
48 final EdgeInsets padding; 49 final EdgeInsets padding;
49 50
  51 + final double maxWidth;
  52 +
50 Widget _buildLegend(Context context, Dataset dataset) { 53 Widget _buildLegend(Context context, Dataset dataset) {
51 final style = Theme.of(context).defaultTextStyle.merge(textStyle); 54 final style = Theme.of(context).defaultTextStyle.merge(textStyle);
52 55
@@ -59,10 +62,14 @@ class ChartLegend extends StatelessWidget { @@ -59,10 +62,14 @@ class ChartLegend extends StatelessWidget {
59 margin: const EdgeInsets.only(right: 5), 62 margin: const EdgeInsets.only(right: 5),
60 child: dataset.legendShape(), 63 child: dataset.legendShape(),
61 ), 64 ),
62 - Text(  
63 - dataset.legend!,  
64 - style: textStyle,  
65 - ) 65 + ConstrainedBox(
  66 + constraints: BoxConstraints(maxWidth: maxWidth),
  67 + child: Text(
  68 + dataset.legend!,
  69 + style: textStyle,
  70 + softWrap: false,
  71 + ),
  72 + ),
66 ], 73 ],
67 ); 74 );
68 } 75 }
  1 +// ignore_for_file: public_member_api_docs
  2 +
  3 +import 'dart:math';
  4 +
  5 +import 'package:pdf/pdf.dart';
  6 +import 'package:pdf/widgets.dart';
  7 +import 'package:vector_math/vector_math_64.dart';
  8 +
  9 +class PieGrid extends ChartGrid {
  10 + PieGrid();
  11 +
  12 + late PdfRect gridBox;
  13 +
  14 + late double total;
  15 +
  16 + late double unit;
  17 +
  18 + late double pieSize;
  19 +
  20 + @override
  21 + void layout(Context context, BoxConstraints constraints,
  22 + {bool parentUsesSize = false}) {
  23 + super.layout(context, constraints, parentUsesSize: parentUsesSize);
  24 +
  25 + final datasets = Chart.of(context).datasets;
  26 + final size = constraints.biggest;
  27 +
  28 + gridBox = PdfRect(0, 0, size.x, size.y);
  29 +
  30 + total = 0.0;
  31 +
  32 + for (final dataset in datasets) {
  33 + assert(dataset is PieDataSet, 'Use only PieDataSet with a PieGrid');
  34 + if (dataset is PieDataSet) {
  35 + total += dataset.value;
  36 + }
  37 + }
  38 +
  39 + unit = pi / total * 2;
  40 + var angle = 0.0;
  41 +
  42 + for (final dataset in datasets) {
  43 + if (dataset is PieDataSet) {
  44 + dataset.angleStart = angle;
  45 + angle += dataset.value * unit;
  46 + dataset.angleEnd = angle;
  47 + }
  48 + }
  49 +
  50 + pieSize = min(gridBox.width / 2, gridBox.height / 2);
  51 + var reduce = false;
  52 +
  53 + do {
  54 + reduce = false;
  55 + for (final dataset in datasets) {
  56 + if (dataset is PieDataSet) {
  57 + dataset.layout(context, BoxConstraints.tight(gridBox.size));
  58 + assert(dataset.box != null);
  59 + if (pieSize > 20 &&
  60 + (dataset.box!.width > gridBox.width ||
  61 + dataset.box!.height > gridBox.height)) {
  62 + pieSize -= 10;
  63 + reduce = true;
  64 + break;
  65 + }
  66 + }
  67 + }
  68 + } while (reduce);
  69 + }
  70 +
  71 + @override
  72 + PdfPoint toChart(PdfPoint p) {
  73 + return p;
  74 + }
  75 +
  76 + void clip(Context context, PdfPoint size) {}
  77 +
  78 + @override
  79 + void paint(Context context) {
  80 + super.paint(context);
  81 +
  82 + final datasets = Chart.of(context).datasets;
  83 +
  84 + context.canvas
  85 + ..saveContext()
  86 + ..setTransform(
  87 + Matrix4.translationValues(box!.width / 2, box!.height / 2, 0),
  88 + );
  89 +
  90 + for (var dataSet in datasets) {
  91 + if (dataSet is PieDataSet) {
  92 + dataSet.paintBackground(context);
  93 + }
  94 + }
  95 +
  96 + for (var dataSet in datasets) {
  97 + if (dataSet is PieDataSet) {
  98 + dataSet.paint(context);
  99 + }
  100 + }
  101 +
  102 + for (var dataSet in datasets) {
  103 + if (dataSet is PieDataSet) {
  104 + dataSet.paintLegend(context);
  105 + }
  106 + }
  107 +
  108 + context.canvas.restoreContext();
  109 + }
  110 +}
  111 +
  112 +enum PieLegendPosition { none, auto, inside }
  113 +
  114 +class PieDataSet extends Dataset {
  115 + PieDataSet({
  116 + required this.value,
  117 + String? legend,
  118 + required PdfColor color,
  119 + this.borderColor = PdfColors.white,
  120 + this.borderWidth = 1.5,
  121 + bool? drawBorder,
  122 + this.drawSurface = true,
  123 + this.surfaceOpacity = 1,
  124 + this.offset = 0,
  125 + this.legendStyle,
  126 + this.legendPosition = PieLegendPosition.auto,
  127 + }) : drawBorder = drawBorder ?? borderColor != null && color != borderColor,
  128 + assert((drawBorder ?? borderColor != null && color != borderColor) ||
  129 + drawSurface),
  130 + super(
  131 + legend: legend,
  132 + color: color,
  133 + );
  134 +
  135 + final double value;
  136 +
  137 + late double angleStart;
  138 +
  139 + late double angleEnd;
  140 +
  141 + final bool drawBorder;
  142 + final PdfColor? borderColor;
  143 + final double borderWidth;
  144 +
  145 + final bool drawSurface;
  146 +
  147 + final double surfaceOpacity;
  148 +
  149 + final double offset;
  150 +
  151 + final TextStyle? legendStyle;
  152 +
  153 + final PieLegendPosition legendPosition;
  154 +
  155 + @override
  156 + void layout(Context context, BoxConstraints constraints,
  157 + {bool parentUsesSize = false}) {
  158 + // final size = constraints.biggest;
  159 +
  160 + // ignore: avoid_as
  161 + final grid = Chart.of(context).grid as PieGrid;
  162 + final len = grid.pieSize + offset;
  163 +
  164 + box = PdfRect(-len, -len, len * 2, len * 2);
  165 + }
  166 +
  167 + void _shape(Context context) {
  168 + // ignore: avoid_as
  169 + final grid = Chart.of(context).grid as PieGrid;
  170 +
  171 + final bisect = (angleStart + angleEnd) / 2;
  172 +
  173 + final cx = sin(bisect) * offset;
  174 + final cy = cos(bisect) * offset;
  175 +
  176 + final sx = cx + sin(angleStart) * grid.pieSize;
  177 + final sy = cy + cos(angleStart) * grid.pieSize;
  178 + final ex = cx + sin(angleEnd) * grid.pieSize;
  179 + final ey = cy + cos(angleEnd) * grid.pieSize;
  180 +
  181 + context.canvas
  182 + ..moveTo(cx, cy)
  183 + ..lineTo(sx, sy)
  184 + ..bezierArc(sx, sy, grid.pieSize, grid.pieSize, ex, ey,
  185 + large: angleEnd - angleStart > pi);
  186 + }
  187 +
  188 + @override
  189 + void paintBackground(Context context) {
  190 + super.paint(context);
  191 +
  192 + if (drawSurface) {
  193 + _shape(context);
  194 + if (surfaceOpacity != 1) {
  195 + context.canvas
  196 + ..saveContext()
  197 + ..setGraphicState(
  198 + PdfGraphicState(opacity: surfaceOpacity),
  199 + );
  200 + }
  201 +
  202 + context.canvas
  203 + ..setFillColor(color)
  204 + ..fillPath();
  205 +
  206 + if (surfaceOpacity != 1) {
  207 + context.canvas.restoreContext();
  208 + }
  209 + }
  210 + }
  211 +
  212 + @override
  213 + void paint(Context context) {
  214 + super.paint(context);
  215 +
  216 + if (drawBorder) {
  217 + _shape(context);
  218 + context.canvas
  219 + ..setLineWidth(borderWidth)
  220 + ..setLineJoin(PdfLineJoin.round)
  221 + ..setStrokeColor(borderColor ?? color)
  222 + ..strokePath(close: true);
  223 + }
  224 + }
  225 +
  226 + void paintLegend(Context context) {
  227 + if (legendPosition != PieLegendPosition.none && legend != null) {
  228 + // ignore: avoid_as
  229 + final grid = Chart.of(context).grid as PieGrid;
  230 +
  231 + final bisect = (angleStart + angleEnd) / 2;
  232 +
  233 + final o = grid.pieSize * 2 / 3;
  234 + final cx = sin(bisect) * (offset + o);
  235 + final cy = cos(bisect) * (offset + o);
  236 +
  237 + Widget.draw(
  238 + Text(legend!, style: legendStyle, textAlign: TextAlign.center),
  239 + offset: PdfPoint(cx, cy),
  240 + context: context,
  241 + alignment: Alignment.center,
  242 + constraints: const BoxConstraints(maxWidth: 200, maxHeight: 200),
  243 + );
  244 + }
  245 + }
  246 +}
@@ -28,6 +28,7 @@ export 'src/widgets/chart/grid_cartesian.dart'; @@ -28,6 +28,7 @@ export 'src/widgets/chart/grid_cartesian.dart';
28 export 'src/widgets/chart/grid_radial.dart'; 28 export 'src/widgets/chart/grid_radial.dart';
29 export 'src/widgets/chart/legend.dart'; 29 export 'src/widgets/chart/legend.dart';
30 export 'src/widgets/chart/line_chart.dart'; 30 export 'src/widgets/chart/line_chart.dart';
  31 +export 'src/widgets/chart/pie_chart.dart';
31 export 'src/widgets/clip.dart'; 32 export 'src/widgets/clip.dart';
32 export 'src/widgets/container.dart'; 33 export 'src/widgets/container.dart';
33 export 'src/widgets/content.dart'; 34 export 'src/widgets/content.dart';
@@ -4,7 +4,7 @@ description: A pdf producer for Dart. It can create pdf files for both web or fl @@ -4,7 +4,7 @@ description: A pdf producer for Dart. It can create pdf files for both web or fl
4 homepage: https://github.com/DavBfr/dart_pdf/tree/master/pdf 4 homepage: https://github.com/DavBfr/dart_pdf/tree/master/pdf
5 repository: https://github.com/DavBfr/dart_pdf 5 repository: https://github.com/DavBfr/dart_pdf
6 issue_tracker: https://github.com/DavBfr/dart_pdf/issues 6 issue_tracker: https://github.com/DavBfr/dart_pdf/issues
7 -version: 3.0.2 7 +version: 3.1.0
8 8
9 environment: 9 environment:
10 sdk: ">=2.12.0-0 <3.0.0" 10 sdk: ">=2.12.0-0 <3.0.0"