David PHAM-VAN

Add PieChart

include: package:pedantic/analysis_options.yaml
analyzer:
strong-mode:
implicit-dynamic: false
errors:
missing_required_param: warning
missing_return: warning
linter:
rules:
- always_put_control_body_on_new_line
- avoid_as
- avoid_bool_literals_in_conditional_expressions
- avoid_classes_with_only_static_members
- avoid_field_initializers_in_const_classes
- avoid_function_literals_in_foreach_calls
- avoid_renaming_method_parameters
- avoid_returning_null_for_void
- avoid_slow_async_io
- avoid_unused_constructor_parameters
- avoid_void_async
- await_only_futures
- camel_case_types
- cancel_subscriptions
- control_flow_in_finally
- directives_ordering
- empty_statements
- flutter_style_todos
- hash_and_equals
- implementation_imports
- iterable_contains_unrelated_type
- list_remove_unrelated_type
- no_adjacent_strings_in_list
- non_constant_identifier_names
- omit_local_variable_types
- overridden_fields
- package_api_docs
- package_names
- package_prefixed_library_names
- prefer_asserts_in_initializer_lists
- prefer_const_constructors
- prefer_const_constructors_in_immutables
- prefer_const_declarations
- prefer_const_literals_to_create_immutables
- prefer_final_locals
- prefer_foreach
- prefer_if_elements_to_conditional_expressions
- prefer_initializing_formals
- prefer_inlined_adds
- prefer_typing_uninitialized_variables
- prefer_void_to_null
- sort_constructors_first
- sort_pub_dependencies
- sort_unnamed_constructors_first
- test_types_in_equals
- throw_in_finally
- unnecessary_brace_in_string_interps
- unnecessary_getters_setters
- unnecessary_null_aware_assignments
- unnecessary_overrides
- unnecessary_parenthesis
- unnecessary_statements
- use_full_hex_values_for_flutter_colors
... ...
... ... @@ -25,29 +25,40 @@ Future<Uint8List> generateReport(PdfPageFormat pageFormat) async {
const tableHeaders = ['Category', 'Budget', 'Expense', 'Result'];
const dataTable = [
['Phone', 80, 95, -15],
['Internet', 250, 230, 20],
['Electricity', 300, 375, -75],
['Movies', 85, 80, 5],
['Food', 300, 350, -50],
['Fuel', 650, 550, 100],
['Insurance', 250, 310, -60],
['Phone', 80, 95],
['Internet', 250, 230],
['Electricity', 300, 375],
['Movies', 85, 80],
['Food', 300, 350],
['Fuel', 650, 550],
['Insurance', 250, 310],
];
// Some summary maths
final budget = dataTable
.map((e) => e[1] as num)
.reduce((value, element) => value + element);
final expense = dataTable
.map((e) => e[2] as num)
.reduce((value, element) => value + element);
final baseColor = PdfColors.cyan;
// Create a PDF document.
final document = pw.Document();
final theme = pw.ThemeData.withFont(
base: pw.Font.ttf(await rootBundle.load('assets/open-sans.ttf')),
bold: pw.Font.ttf(await rootBundle.load('assets/open-sans-bold.ttf')),
);
// Add page to the PDF
document.addPage(
pw.Page(
pageFormat: pageFormat,
theme: pw.ThemeData.withFont(
base: pw.Font.ttf(await rootBundle.load('assets/open-sans.ttf')),
bold: pw.Font.ttf(await rootBundle.load('assets/open-sans-bold.ttf')),
),
theme: theme,
build: (context) {
// Top bar chart
final chart1 = pw.Chart(
left: pw.Container(
alignment: pw.Alignment.topCenter,
... ... @@ -117,6 +128,7 @@ Future<Uint8List> generateReport(PdfPageFormat pageFormat) async {
],
);
// Left curved line chart
final chart2 = pw.Chart(
grid: pw.CartesianGrid(
xAxis: pw.FixedAxis([0, 1, 2, 3, 4, 5, 6]),
... ... @@ -143,10 +155,19 @@ Future<Uint8List> generateReport(PdfPageFormat pageFormat) async {
],
);
// Data table
final table = pw.Table.fromTextArray(
border: null,
headers: tableHeaders,
data: dataTable,
data: List<List<dynamic>>.generate(
dataTable.length,
(index) => <dynamic>[
dataTable[index][0],
dataTable[index][1],
dataTable[index][2],
(dataTable[index][1] as num) - (dataTable[index][2] as num),
],
),
headerStyle: pw.TextStyle(
color: PdfColors.white,
fontWeight: pw.FontWeight.bold,
... ... @@ -162,8 +183,11 @@ Future<Uint8List> generateReport(PdfPageFormat pageFormat) async {
),
),
),
cellAlignment: pw.Alignment.centerRight,
cellAlignments: {0: pw.Alignment.centerLeft},
);
// Page layout
return pw.Column(
children: [
pw.Text('Budget Report',
... ... @@ -223,7 +247,7 @@ Future<Uint8List> generateReport(PdfPageFormat pageFormat) async {
),
),
pw.Text(
'Budget was originally \$1915. A total of \$1990 was spent on the month of January which exceeded the overall budget by \$75',
'Budget was originally \$$budget. A total of \$$expense was spent on the month of January which exceeded the overall budget by \$${expense - budget}',
textAlign: pw.TextAlign.justify,
)
],
... ... @@ -237,6 +261,55 @@ Future<Uint8List> generateReport(PdfPageFormat pageFormat) async {
),
);
// Second page with a pie chart
document.addPage(
pw.Page(
pageFormat: pageFormat,
theme: theme,
build: (context) {
const chartColors = [
PdfColors.blue300,
PdfColors.green300,
PdfColors.amber300,
PdfColors.pink300,
PdfColors.cyan300,
PdfColors.purple300,
PdfColors.lime300,
];
return pw.SizedBox(
height: 400,
child: pw.Chart(
title: pw.Text(
'Expense breakdown',
style: pw.TextStyle(
color: baseColor,
fontSize: 20,
),
),
grid: pw.PieGrid(),
datasets: List<pw.Dataset>.generate(dataTable.length, (index) {
final data = dataTable[index];
final color = chartColors[index % chartColors.length];
final textColor =
color.luminance < 0.2 ? PdfColors.white : PdfColors.black;
final value = (data[2] as num).toDouble();
final pct = (value / expense * 100).round();
return pw.PieDataSet(
legend: '${data[0]}\n$pct%',
value: value,
color: color,
legendStyle: pw.TextStyle(fontSize: 10, color: textColor),
);
}),
),
);
},
),
);
// Return the PDF file content
return document.save();
}
... ...
# Changelog
## 3.0.2
## 3.1.0
- Fix some linting issues
- Add PdfPage.rotate attribute
- Add RadialGrid for charts with polar coordinates
- Add PieChart
## 3.0.1
... ...
... ... @@ -35,6 +35,7 @@ class ChartLegend extends StatelessWidget {
this.direction = Axis.vertical,
this.decoration,
this.padding = const EdgeInsets.all(5),
this.maxWidth = 200,
});
final TextStyle? textStyle;
... ... @@ -47,6 +48,8 @@ class ChartLegend extends StatelessWidget {
final EdgeInsets padding;
final double maxWidth;
Widget _buildLegend(Context context, Dataset dataset) {
final style = Theme.of(context).defaultTextStyle.merge(textStyle);
... ... @@ -59,10 +62,14 @@ class ChartLegend extends StatelessWidget {
margin: const EdgeInsets.only(right: 5),
child: dataset.legendShape(),
),
Text(
dataset.legend!,
style: textStyle,
)
ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth),
child: Text(
dataset.legend!,
style: textStyle,
softWrap: false,
),
),
],
);
}
... ...
// ignore_for_file: public_member_api_docs
import 'dart:math';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart';
import 'package:vector_math/vector_math_64.dart';
class PieGrid extends ChartGrid {
PieGrid();
late PdfRect gridBox;
late double total;
late double unit;
late double pieSize;
@override
void layout(Context context, BoxConstraints constraints,
{bool parentUsesSize = false}) {
super.layout(context, constraints, parentUsesSize: parentUsesSize);
final datasets = Chart.of(context).datasets;
final size = constraints.biggest;
gridBox = PdfRect(0, 0, size.x, size.y);
total = 0.0;
for (final dataset in datasets) {
assert(dataset is PieDataSet, 'Use only PieDataSet with a PieGrid');
if (dataset is PieDataSet) {
total += dataset.value;
}
}
unit = pi / total * 2;
var angle = 0.0;
for (final dataset in datasets) {
if (dataset is PieDataSet) {
dataset.angleStart = angle;
angle += dataset.value * unit;
dataset.angleEnd = angle;
}
}
pieSize = min(gridBox.width / 2, gridBox.height / 2);
var reduce = false;
do {
reduce = false;
for (final dataset in datasets) {
if (dataset is PieDataSet) {
dataset.layout(context, BoxConstraints.tight(gridBox.size));
assert(dataset.box != null);
if (pieSize > 20 &&
(dataset.box!.width > gridBox.width ||
dataset.box!.height > gridBox.height)) {
pieSize -= 10;
reduce = true;
break;
}
}
}
} while (reduce);
}
@override
PdfPoint toChart(PdfPoint p) {
return p;
}
void clip(Context context, PdfPoint size) {}
@override
void paint(Context context) {
super.paint(context);
final datasets = Chart.of(context).datasets;
context.canvas
..saveContext()
..setTransform(
Matrix4.translationValues(box!.width / 2, box!.height / 2, 0),
);
for (var dataSet in datasets) {
if (dataSet is PieDataSet) {
dataSet.paintBackground(context);
}
}
for (var dataSet in datasets) {
if (dataSet is PieDataSet) {
dataSet.paint(context);
}
}
for (var dataSet in datasets) {
if (dataSet is PieDataSet) {
dataSet.paintLegend(context);
}
}
context.canvas.restoreContext();
}
}
enum PieLegendPosition { none, auto, inside }
class PieDataSet extends Dataset {
PieDataSet({
required this.value,
String? legend,
required PdfColor color,
this.borderColor = PdfColors.white,
this.borderWidth = 1.5,
bool? drawBorder,
this.drawSurface = true,
this.surfaceOpacity = 1,
this.offset = 0,
this.legendStyle,
this.legendPosition = PieLegendPosition.auto,
}) : drawBorder = drawBorder ?? borderColor != null && color != borderColor,
assert((drawBorder ?? borderColor != null && color != borderColor) ||
drawSurface),
super(
legend: legend,
color: color,
);
final double value;
late double angleStart;
late double angleEnd;
final bool drawBorder;
final PdfColor? borderColor;
final double borderWidth;
final bool drawSurface;
final double surfaceOpacity;
final double offset;
final TextStyle? legendStyle;
final PieLegendPosition legendPosition;
@override
void layout(Context context, BoxConstraints constraints,
{bool parentUsesSize = false}) {
// final size = constraints.biggest;
// ignore: avoid_as
final grid = Chart.of(context).grid as PieGrid;
final len = grid.pieSize + offset;
box = PdfRect(-len, -len, len * 2, len * 2);
}
void _shape(Context context) {
// ignore: avoid_as
final grid = Chart.of(context).grid as PieGrid;
final bisect = (angleStart + angleEnd) / 2;
final cx = sin(bisect) * offset;
final cy = cos(bisect) * offset;
final sx = cx + sin(angleStart) * grid.pieSize;
final sy = cy + cos(angleStart) * grid.pieSize;
final ex = cx + sin(angleEnd) * grid.pieSize;
final ey = cy + cos(angleEnd) * grid.pieSize;
context.canvas
..moveTo(cx, cy)
..lineTo(sx, sy)
..bezierArc(sx, sy, grid.pieSize, grid.pieSize, ex, ey,
large: angleEnd - angleStart > pi);
}
@override
void paintBackground(Context context) {
super.paint(context);
if (drawSurface) {
_shape(context);
if (surfaceOpacity != 1) {
context.canvas
..saveContext()
..setGraphicState(
PdfGraphicState(opacity: surfaceOpacity),
);
}
context.canvas
..setFillColor(color)
..fillPath();
if (surfaceOpacity != 1) {
context.canvas.restoreContext();
}
}
}
@override
void paint(Context context) {
super.paint(context);
if (drawBorder) {
_shape(context);
context.canvas
..setLineWidth(borderWidth)
..setLineJoin(PdfLineJoin.round)
..setStrokeColor(borderColor ?? color)
..strokePath(close: true);
}
}
void paintLegend(Context context) {
if (legendPosition != PieLegendPosition.none && legend != null) {
// ignore: avoid_as
final grid = Chart.of(context).grid as PieGrid;
final bisect = (angleStart + angleEnd) / 2;
final o = grid.pieSize * 2 / 3;
final cx = sin(bisect) * (offset + o);
final cy = cos(bisect) * (offset + o);
Widget.draw(
Text(legend!, style: legendStyle, textAlign: TextAlign.center),
offset: PdfPoint(cx, cy),
context: context,
alignment: Alignment.center,
constraints: const BoxConstraints(maxWidth: 200, maxHeight: 200),
);
}
}
}
... ...
... ... @@ -28,6 +28,7 @@ export 'src/widgets/chart/grid_cartesian.dart';
export 'src/widgets/chart/grid_radial.dart';
export 'src/widgets/chart/legend.dart';
export 'src/widgets/chart/line_chart.dart';
export 'src/widgets/chart/pie_chart.dart';
export 'src/widgets/clip.dart';
export 'src/widgets/container.dart';
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
homepage: https://github.com/DavBfr/dart_pdf/tree/master/pdf
repository: https://github.com/DavBfr/dart_pdf
issue_tracker: https://github.com/DavBfr/dart_pdf/issues
version: 3.0.2
version: 3.1.0
environment:
sdk: ">=2.12.0-0 <3.0.0"
... ...