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
22 changes: 20 additions & 2 deletions lib/src/chart/pie_chart/pie_chart_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,10 @@ class PieChartSectionData with EquatableMixin {
/// by default it draws the widget at the middle of section, but you can change the
/// [badgePositionPercentageOffset] to have your desire design,
/// the value works the same way as [titlePositionPercentageOffset].
///
/// [borderRadius] defines the radius of rounded corners for the section.
/// Set to 0 for sharp corners (default).
/// Maximum supported value is 8.0 - values above this may cause visual artifacts.
PieChartSectionData({
double? value,
Color? color,
Expand All @@ -165,14 +169,20 @@ class PieChartSectionData with EquatableMixin {
this.badgeWidget,
double? titlePositionPercentageOffset,
double? badgePositionPercentageOffset,
}) : value = value ?? 10,
double? borderRadius,
}) : assert(
(borderRadius ?? 0) <= 8.0,
'borderRadius must be <= 8.0. Values above 8.0 are not supported and may cause visual artifacts.',
),
value = value ?? 10,
color = color ?? Colors.cyan,
radius = radius ?? 40,
showTitle = showTitle ?? true,
title = title ?? (value == null ? '' : value.toString()),
borderSide = borderSide ?? const BorderSide(width: 0),
titlePositionPercentageOffset = titlePositionPercentageOffset ?? 0.5,
badgePositionPercentageOffset = badgePositionPercentageOffset ?? 0.5;
badgePositionPercentageOffset = badgePositionPercentageOffset ?? 0.5,
borderRadius = borderRadius ?? 0;

/// It determines how much space it should occupy around the circle.
///
Expand Down Expand Up @@ -223,6 +233,10 @@ class PieChartSectionData with EquatableMixin {
/// 1.0 means near the outside of the [PieChart].
final double badgePositionPercentageOffset;

/// Defines the radius of rounded corners for the section.
/// Set to 0 for sharp corners.
final double borderRadius;

/// Copies current [PieChartSectionData] to a new [PieChartSectionData],
/// and replaces provided values.
PieChartSectionData copyWith({
Expand All @@ -237,6 +251,7 @@ class PieChartSectionData with EquatableMixin {
Widget? badgeWidget,
double? titlePositionPercentageOffset,
double? badgePositionPercentageOffset,
double? borderRadius,
}) =>
PieChartSectionData(
value: value ?? this.value,
Expand All @@ -252,6 +267,7 @@ class PieChartSectionData with EquatableMixin {
titlePositionPercentageOffset ?? this.titlePositionPercentageOffset,
badgePositionPercentageOffset:
badgePositionPercentageOffset ?? this.badgePositionPercentageOffset,
borderRadius: borderRadius ?? this.borderRadius,
);

/// Lerps a [PieChartSectionData] based on [t] value, check [Tween.lerp].
Expand Down Expand Up @@ -280,6 +296,7 @@ class PieChartSectionData with EquatableMixin {
b.badgePositionPercentageOffset,
t,
),
borderRadius: lerpDouble(a.borderRadius, b.borderRadius, t),
);

/// Used for equality check, see [EquatableMixin].
Expand All @@ -296,6 +313,7 @@ class PieChartSectionData with EquatableMixin {
badgeWidget,
titlePositionPercentageOffset,
badgePositionPercentageOffset,
borderRadius,
];
}

Expand Down
187 changes: 178 additions & 9 deletions lib/src/chart/pie_chart/pie_chart_painter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -217,17 +217,77 @@ class PieChartPainter extends BaseChartPainter<PieChartData> {
final endLineTo = endLineFrom + endLineDirection * section.radius;
final endLine = Line(endLineFrom, endLineTo);

var sectionPath = Path()
..moveTo(startLine.from.dx, startLine.from.dy)
..lineTo(startLine.to.dx, startLine.to.dy)
..arcTo(sectionRadiusRect, startRadians, sweepRadians, false)
..lineTo(endLine.from.dx, endLine.from.dy)
..arcTo(centerRadiusRect, endRadians, -sweepRadians, false)
..moveTo(startLine.from.dx, startLine.from.dy)
..close();
Path sectionPath;

// Apply rounded corners if borderRadius is set
if (section.borderRadius > 0) {
// Calculate the angular offset for section spacing
final spaceAngleOffset = sectionSpace > 0
? math.atan2(sectionSpace / 2, centerRadius + section.radius / 2)
: 0.0;

// Adjust start and end angles to account for section spacing
final adjustedStartRadians = startRadians + spaceAngleOffset;
final adjustedEndRadians = endRadians - spaceAngleOffset;
final adjustedSweepRadians = sweepRadians - (2 * spaceAngleOffset);

// Recalculate lines with adjusted angles
final adjustedStartLineDirection = Offset(
math.cos(adjustedStartRadians),
math.sin(adjustedStartRadians),
);
final adjustedStartLineFrom =
center + adjustedStartLineDirection * centerRadius;
final adjustedStartLineTo =
adjustedStartLineFrom + adjustedStartLineDirection * section.radius;

final adjustedEndLineDirection = Offset(
math.cos(adjustedEndRadians),
math.sin(adjustedEndRadians),
);
final adjustedEndLineFrom =
center + adjustedEndLineDirection * centerRadius;
final adjustedEndLineTo =
adjustedEndLineFrom + adjustedEndLineDirection * section.radius;

// Clamp the border radius to ensure it doesn't exceed available space
final maxRadiusForWidth = section.radius / 2;
final arcLength =
(centerRadius + section.radius / 2) * adjustedSweepRadians;
final maxRadiusForArc = arcLength / 2;
final effectiveRadius = math.min(
section.borderRadius,
math.min(maxRadiusForWidth, maxRadiusForArc),
);

sectionPath = _generateRoundedSectionPath(
Line(adjustedStartLineFrom, adjustedStartLineTo),
Line(adjustedEndLineFrom, adjustedEndLineTo),
adjustedStartRadians,
adjustedSweepRadians,
adjustedEndRadians,
center,
centerRadius,
section.radius,
effectiveRadius,
sectionRadiusRect,
centerRadiusRect,
);
} else {
// Original sharp-corner path
sectionPath = Path()
..moveTo(startLine.from.dx, startLine.from.dy)
..lineTo(startLine.to.dx, startLine.to.dy)
..arcTo(sectionRadiusRect, startRadians, sweepRadians, false)
..lineTo(endLine.from.dx, endLine.from.dy)
..arcTo(centerRadiusRect, endRadians, -sweepRadians, false)
..moveTo(startLine.from.dx, startLine.from.dy)
..close();
}

/// Subtract section space from the sectionPath
if (sectionSpace != 0) {
/// (Skip this for rounded corners since spacing is already accounted for)
if (sectionSpace != 0 && section.borderRadius == 0) {
final startLineSeparatorPath = createRectPathAroundLine(
Line(startLineFrom, startLineTo),
sectionSpace,
Expand Down Expand Up @@ -260,6 +320,115 @@ class PieChartPainter extends BaseChartPainter<PieChartData> {
return sectionPath;
}

/// Generates a path with rounded corners for a pie section
Path _generateRoundedSectionPath(
Line startLine,
Line endLine,
double startRadians,
double sweepRadians,
double endRadians,
Offset center,
double centerRadius,
double sectionRadius,
double cornerRadius,
Rect sectionRadiusRect,
Rect centerRadiusRect,
) {
// Calculate the four corners
final outerRadius = centerRadius + sectionRadius;
final innerRadius = centerRadius;

// Corner 1: Start line inner (where start line meets inner arc)
final corner1End = center +
Offset(
math.cos(startRadians) * (innerRadius + cornerRadius),
math.sin(startRadians) * (innerRadius + cornerRadius),
);

// Corner 2: Start line outer (where start line meets outer arc)
final corner2Start = center +
Offset(
math.cos(startRadians) * (outerRadius - cornerRadius),
math.sin(startRadians) * (outerRadius - cornerRadius),
);
final corner2End = center +
Offset(
math.cos(startRadians) * outerRadius,
math.sin(startRadians) * outerRadius,
) +
Offset(
math.cos(startRadians + math.pi / 2) * cornerRadius,
math.sin(startRadians + math.pi / 2) * cornerRadius,
);

// Corner 3: End line outer (where outer arc meets end line)
final corner3End = center +
Offset(
math.cos(endRadians) * (outerRadius - cornerRadius),
math.sin(endRadians) * (outerRadius - cornerRadius),
);

// Corner 4: End line inner (where end line meets inner arc)
final corner4Start = center +
Offset(
math.cos(endRadians) * (innerRadius + cornerRadius),
math.sin(endRadians) * (innerRadius + cornerRadius),
);
final corner4End = center +
Offset(
math.cos(endRadians) * innerRadius,
math.sin(endRadians) * innerRadius,
) +
Offset(
math.cos(endRadians - math.pi / 2) * cornerRadius,
math.sin(endRadians - math.pi / 2) * cornerRadius,
);

// Calculate adjusted angles for the main arcs
final outerArcAngleOffset = cornerRadius / outerRadius;
final innerArcAngleOffset = cornerRadius / innerRadius;

// Build the path
final adjustedOuterStartAngle = startRadians + outerArcAngleOffset;
final adjustedOuterSweepAngle = sweepRadians - (2 * outerArcAngleOffset);
final adjustedInnerStartAngle = endRadians - innerArcAngleOffset;
final adjustedInnerSweepAngle = -(sweepRadians - (2 * innerArcAngleOffset));

return Path()
..moveTo(corner1End.dx, corner1End.dy)
..lineTo(corner2Start.dx, corner2Start.dy)
..arcToPoint(
corner2End,
radius: Radius.circular(cornerRadius),
)
..arcTo(
sectionRadiusRect,
adjustedOuterStartAngle,
adjustedOuterSweepAngle,
false,
)
..arcToPoint(
corner3End,
radius: Radius.circular(cornerRadius),
)
..lineTo(corner4Start.dx, corner4Start.dy)
..arcToPoint(
corner4End,
radius: Radius.circular(cornerRadius),
)
..arcTo(
centerRadiusRect,
adjustedInnerStartAngle,
adjustedInnerSweepAngle,
false,
)
..arcToPoint(
corner1End,
radius: Radius.circular(cornerRadius),
)
..close();
}

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