Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ EXTERNAL SOURCES:

SPEC CHECKSUMS:
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b

PODFILE CHECKSUM: 0dbd5a87e0ace00c9610d2037ac22083a01f861d

Expand Down
9 changes: 9 additions & 0 deletions lib/src/chart/pie_chart/pie_chart_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ class PieChartSectionData with EquatableMixin {
this.titleStyle,
String? title,
BorderSide? borderSide,
double? cornerRadius,
this.badgeWidget,
double? titlePositionPercentageOffset,
double? badgePositionPercentageOffset,
Expand All @@ -171,6 +172,7 @@ class PieChartSectionData with EquatableMixin {
showTitle = showTitle ?? true,
title = title ?? (value == null ? '' : value.toString()),
borderSide = borderSide ?? const BorderSide(width: 0),
cornerRadius = cornerRadius ?? 0.0,
titlePositionPercentageOffset = titlePositionPercentageOffset ?? 0.5,
badgePositionPercentageOffset = badgePositionPercentageOffset ?? 0.5;

Expand Down Expand Up @@ -203,6 +205,9 @@ class PieChartSectionData with EquatableMixin {
/// Defines border stroke around the section
final BorderSide borderSide;

/// Defines corner radius for rounded edges (applies to all corners)
final double cornerRadius;

/// Defines a widget that represents the section.
///
/// This can be anything from a text, an image, an animation, and even a combination of widgets.
Expand Down Expand Up @@ -234,6 +239,7 @@ class PieChartSectionData with EquatableMixin {
TextStyle? titleStyle,
String? title,
BorderSide? borderSide,
double? cornerRadius,
Widget? badgeWidget,
double? titlePositionPercentageOffset,
double? badgePositionPercentageOffset,
Expand All @@ -247,6 +253,7 @@ class PieChartSectionData with EquatableMixin {
titleStyle: titleStyle ?? this.titleStyle,
title: title ?? this.title,
borderSide: borderSide ?? this.borderSide,
cornerRadius: cornerRadius ?? this.cornerRadius,
badgeWidget: badgeWidget ?? this.badgeWidget,
titlePositionPercentageOffset:
titlePositionPercentageOffset ?? this.titlePositionPercentageOffset,
Expand All @@ -269,6 +276,7 @@ class PieChartSectionData with EquatableMixin {
titleStyle: TextStyle.lerp(a.titleStyle, b.titleStyle, t),
title: b.title,
borderSide: BorderSide.lerp(a.borderSide, b.borderSide, t),
cornerRadius: lerpDouble(a.cornerRadius, b.cornerRadius, t),
badgeWidget: b.badgeWidget,
titlePositionPercentageOffset: lerpDouble(
a.titlePositionPercentageOffset,
Expand All @@ -293,6 +301,7 @@ class PieChartSectionData with EquatableMixin {
titleStyle,
title,
borderSide,
cornerRadius,
badgeWidget,
titlePositionPercentageOffset,
badgePositionPercentageOffset,
Expand Down
277 changes: 275 additions & 2 deletions lib/src/chart/pie_chart/pie_chart_painter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,10 @@ class PieChartPainter extends BaseChartPainter<PieChartData> {
final endLineTo = endLineFrom + endLineDirection * section.radius;
final endLine = Line(endLineFrom, endLineTo);

var sectionPath = Path()
var sectionPath = Path();

// First create the basic section path (without rounding)
sectionPath = Path()
..moveTo(startLine.from.dx, startLine.from.dy)
..lineTo(startLine.to.dx, startLine.to.dy)
..arcTo(sectionRadiusRect, startRadians, sweepRadians, false)
Expand All @@ -226,7 +229,7 @@ class PieChartPainter extends BaseChartPainter<PieChartData> {
..moveTo(startLine.from.dx, startLine.from.dy)
..close();

/// Subtract section space from the sectionPath
/// First apply section space separators to the basic path
if (sectionSpace != 0) {
final startLineSeparatorPath = createRectPathAroundLine(
Line(startLineFrom, startLineTo),
Expand Down Expand Up @@ -257,9 +260,279 @@ class PieChartPainter extends BaseChartPainter<PieChartData> {
}
}

// Then apply border radius to the resulting separated path
if (section.cornerRadius > 0) {
// Get the bounds of the separated path
final pathBounds = sectionPath.getBounds();
if (!pathBounds.isEmpty) {
// We need to calculate new angles for the separated section
// to apply rounding correctly to the actual shape we have

// Calculate effective angles after separation
final separatorAngleReduction = sectionSpace != 0
? math.atan2(sectionSpace, centerRadius + section.radius / 2)
: 0.0;

final effectiveStartRadians = startRadians + separatorAngleReduction;
final effectiveSweepRadians =
sweepRadians - (2 * separatorAngleReduction);

if (effectiveSweepRadians > 0) {
// Create new rects for the adjusted geometry
final effectiveSectionRadiusRect = Rect.fromCircle(
center: center,
radius: centerRadius + section.radius,
);

final effectiveCenterRadiusRect = Rect.fromCircle(
center: center,
radius: centerRadius,
);

// Generate rounded path with the effective angles
sectionPath = generateRoundedSectionPath(
section,
effectiveStartRadians,
effectiveSweepRadians,
center,
centerRadius,
effectiveSectionRadiusRect,
effectiveCenterRadiusRect,
);
}
}
}

return sectionPath;
}

/// Generates a Path for a pie-section with rounded corners.
///
/// This method builds a path that rounds both the outer and inner
/// corners of a pie section (when `centerRadius > 0`). It clamps the
/// requested `section.cornerRadius` separately for the outer and inner
/// edges to avoid geometric overlap when the section is narrow or the
/// radii would be too large for the available arc length.
///
/// Important behaviors / notes:
/// - If `cornerRadius <= 1` the method returns a standard (non-rounded)
/// section path for performance and to avoid tiny visual artifacts.
/// - Outer and inner corner radii are clamped independently (`clampedOuterRadius`
/// and `clampedInnerRadius`) to reasonable maxima based on section size
/// and sweep angle.
/// - The code supports `centerRadius == 0` (fully filled pie) and
/// `centerRadius > 0` (donut). When `centerRadius > 0` the inner
/// corners are rounded as well.
/// - `sectionsSpace` trimming is applied later by subtracting separator
/// rectangles from the resulting path (see `generateSectionPath`).
/// - There are known platform/engine caveats when using `Path.combine` on
/// web-html renderer; the subtraction steps are guarded with try/catch
/// where used.
@visibleForTesting
Path generateRoundedSectionPath(
PieChartSectionData section,
double startRadians,
double sweepRadians,
Offset center,
double centerRadius,
Rect sectionRadiusRect,
Rect centerRadiusRect,
) {
final endRadians = startRadians + sweepRadians;
final outerRadius = centerRadius + section.radius;
// User-provided corner radius (applies uniformly to this section).
final cornerRadius = section.cornerRadius;

final path = Path();

if (cornerRadius <= 1) {
// if corner radius is too small, return standard section path
final innerStart = center +
Offset(math.cos(startRadians), math.sin(startRadians)) * centerRadius;
final outerStart = center +
Offset(math.cos(startRadians), math.sin(startRadians)) * outerRadius;
final innerEnd = center +
Offset(math.cos(endRadians), math.sin(endRadians)) * centerRadius;

path
..moveTo(innerStart.dx, innerStart.dy)
..lineTo(outerStart.dx, outerStart.dy)
..arcTo(sectionRadiusRect, startRadians, sweepRadians, false)
..lineTo(innerEnd.dx, innerEnd.dy)
..arcTo(centerRadiusRect, endRadians, -sweepRadians, false)
..close();
} else {
// Clamp requested radii to avoid overlaps. We compute a separate
// maximum for the outer arc (based on section radius and sweep angle)
// and for the inner arc (based on centerRadius). This keeps rounding
// visually stable across different section sizes.
final maxRadiusForSection =
math.min(section.radius * 0.3, sweepRadians * outerRadius * 0.15);
final maxRadiusForCenter = centerRadius > 0
? math.min(centerRadius * 0.3, sweepRadians * centerRadius * 0.15)
: 0.0;
final clampedOuterRadius = math.min(cornerRadius, maxRadiusForSection);
final clampedInnerRadius = math.min(cornerRadius, maxRadiusForCenter);

// Compute angular offsets that correspond to the linear corner radii.
// These are used to trim the sweep angles so the rounded joins fit
// cleanly along the arc.
final outerAngleOffset =
outerRadius > 0 ? clampedOuterRadius / outerRadius : 0.0;
final innerAngleOffset =
centerRadius > 0 ? clampedInnerRadius / centerRadius : 0.0;

// Tight angles for outside corners
final outerStartAngle = startRadians + outerAngleOffset;
final outerEndAngle = endRadians - outerAngleOffset;
final outerSweepAngle = sweepRadians - (2 * outerAngleOffset);

// Tight angles for inside corners
final innerStartAngle = startRadians + innerAngleOffset;
final innerEndAngle = endRadians - innerAngleOffset;
final innerSweepAngle = sweepRadians - (2 * innerAngleOffset);

// Points of the outer corners
final outerStartPoint = center +
Offset(math.cos(startRadians), math.sin(startRadians)) * outerRadius;
final outerEndPoint = center +
Offset(math.cos(endRadians), math.sin(endRadians)) * outerRadius;
final outerStartRounded = center +
Offset(math.cos(outerStartAngle), math.sin(outerStartAngle)) *
outerRadius;
final outerEndRounded = center +
Offset(math.cos(outerEndAngle), math.sin(outerEndAngle)) *
outerRadius;

// Points of the inner corners
final innerStartPoint = center +
Offset(math.cos(startRadians), math.sin(startRadians)) * centerRadius;
final innerEndPoint = center +
Offset(math.cos(endRadians), math.sin(endRadians)) * centerRadius;
final innerStartRounded = center +
Offset(math.cos(innerStartAngle), math.sin(innerStartAngle)) *
centerRadius;
final innerEndRounded = center +
Offset(math.cos(innerEndAngle), math.sin(innerEndAngle)) *
centerRadius;

// Control points used to connect the rounded corner bezier segments to
// the inner/outer arcs. They lie along the original radial directions
// but offset inward/outward by the clamped radii.
final startOuterControl = center +
Offset(math.cos(startRadians), math.sin(startRadians)) *
(outerRadius - clampedOuterRadius);
final endOuterControl = center +
Offset(math.cos(endRadians), math.sin(endRadians)) *
(outerRadius - clampedOuterRadius);
final startInnerControl = center +
Offset(math.cos(startRadians), math.sin(startRadians)) *
(centerRadius + clampedInnerRadius);
final endInnerControl = center +
Offset(math.cos(endRadians), math.sin(endRadians)) *
(centerRadius + clampedInnerRadius);

// Build the rounded path step by step
if (centerRadius > 0) {
// Start from the inner rounded corner
path.moveTo(innerStartRounded.dx, innerStartRounded.dy);

// Inner starting rounded corner (quadratic join). If the inner
// radius is small we fall back to a straight line to avoid tiny
// bezier segments.
if (clampedInnerRadius > 1) {
path.quadraticBezierTo(
innerStartPoint.dx,
innerStartPoint.dy,
startInnerControl.dx,
startInnerControl.dy,
);
} else {
path.lineTo(innerStartPoint.dx, innerStartPoint.dy);
}

// Straight line to the outer edge
path.lineTo(startOuterControl.dx, startOuterControl.dy);

// Outer starting rounded corner (quadratic join).
if (clampedOuterRadius > 1) {
path.quadraticBezierTo(
outerStartPoint.dx,
outerStartPoint.dy,
outerStartRounded.dx,
outerStartRounded.dy,
);
} else {
path.lineTo(outerStartPoint.dx, outerStartPoint.dy);
}
} else {
// If there is no centerRadius, start from the center
path
..moveTo(center.dx, center.dy)
..lineTo(startOuterControl.dx, startOuterControl.dy);

if (clampedOuterRadius > 1) {
path.quadraticBezierTo(
outerStartPoint.dx,
outerStartPoint.dy,
outerStartRounded.dx,
outerStartRounded.dy,
);
} else {
path.lineTo(outerStartPoint.dx, outerStartPoint.dy);
}
}

// Draw the outer arc between the two rounded outer corner points.
if (outerSweepAngle > 0) {
path.arcTo(sectionRadiusRect, outerStartAngle, outerSweepAngle, false);
}

// Outer ending rounded corner (quadratic join).
if (clampedOuterRadius > 1) {
path
..lineTo(outerEndRounded.dx, outerEndRounded.dy)
..quadraticBezierTo(
outerEndPoint.dx,
outerEndPoint.dy,
endOuterControl.dx,
endOuterControl.dy,
);
} else {
path.lineTo(outerEndPoint.dx, outerEndPoint.dy);
}

if (centerRadius > 0) {
// Straight line to the inner edge
path.lineTo(endInnerControl.dx, endInnerControl.dy);

// Inner ending rounded corner (quadratic join).
if (clampedInnerRadius > 1) {
path.quadraticBezierTo(
innerEndPoint.dx,
innerEndPoint.dy,
innerEndRounded.dx,
innerEndRounded.dy,
);
} else {
path.lineTo(innerEndPoint.dx, innerEndPoint.dy);
}

// Draw the inner arc between the two rounded inner corner points.
if (innerSweepAngle > 0) {
path.arcTo(centerRadiusRect, innerEndAngle, -innerSweepAngle, false);
}
} else {
// If there is no centerRadius, close towards the center
path.lineTo(center.dx, center.dy);
}

path.close();
}

return path;
}

/// Creates a rect around a narrow line
@visibleForTesting
Path createRectPathAroundLine(Line line, double width) {
Expand Down