Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pie Chart Connector Lines feature #1732

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
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
1 change: 1 addition & 0 deletions lib/fl_chart.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export 'src/chart/base/base_chart/fl_touch_event.dart';
export 'src/chart/line_chart/line_chart.dart';
export 'src/chart/line_chart/line_chart_data.dart';
export 'src/chart/pie_chart/pie_chart.dart';
export 'src/chart/pie_chart/pie_chart_connector_lines.dart';
export 'src/chart/pie_chart/pie_chart_data.dart';
export 'src/chart/radar_chart/radar_chart.dart';
export 'src/chart/radar_chart/radar_chart_data.dart';
Expand Down
12 changes: 12 additions & 0 deletions lib/src/chart/pie_chart/pie_chart_connector_lines.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import 'package:flutter/material.dart';

class FLConnectorLineSettings {
const FLConnectorLineSettings({
this.length,
double? width,
this.color,
}) : width = width ?? 1.0;
final String? length;
final double width;
final Color? color;
}
81 changes: 40 additions & 41 deletions lib/src/chart/pie_chart/pie_chart_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:ui';

import 'package:equatable/equatable.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:fl_chart/src/chart/pie_chart/pie_chart_connector_lines.dart';
import 'package:fl_chart/src/utils/lerp.dart';
import 'package:flutter/material.dart';

Expand Down Expand Up @@ -32,17 +33,17 @@ class PieChartData extends BaseChartData with EquatableMixin {
FlBorderData? borderData,
bool? titleSunbeamLayout,
}) : sections = sections?.where((element) => element.value != 0).toList() ??
const [],
const [],
centerSpaceRadius = centerSpaceRadius ?? double.infinity,
centerSpaceColor = centerSpaceColor ?? Colors.transparent,
sectionsSpace = sectionsSpace ?? 2,
startDegreeOffset = startDegreeOffset ?? 0,
pieTouchData = pieTouchData ?? PieTouchData(),
titleSunbeamLayout = titleSunbeamLayout ?? false,
super(
borderData: borderData ?? FlBorderData(show: false),
touchData: pieTouchData ?? PieTouchData(),
);
borderData: borderData ?? FlBorderData(show: false),
touchData: pieTouchData ?? PieTouchData(),
);

/// Defines showing sections of the [PieChart].
final List<PieChartSectionData> sections;
Expand Down Expand Up @@ -112,7 +113,7 @@ class PieChartData extends BaseChartData with EquatableMixin {
pieTouchData: b.pieTouchData,
sectionsSpace: lerpDouble(a.sectionsSpace, b.sectionsSpace, t),
startDegreeOffset:
lerpDouble(a.startDegreeOffset, b.startDegreeOffset, t),
lerpDouble(a.startDegreeOffset, b.startDegreeOffset, t),
sections: lerpPieChartSectionDataList(a.sections, b.sections, t),
titleSunbeamLayout: b.titleSunbeamLayout,
);
Expand All @@ -124,15 +125,15 @@ class PieChartData extends BaseChartData with EquatableMixin {
/// Used for equality check, see [EquatableMixin].
@override
List<Object?> get props => [
sections,
centerSpaceRadius,
centerSpaceColor,
pieTouchData,
sectionsSpace,
startDegreeOffset,
borderData,
titleSunbeamLayout,
];
sections,
centerSpaceRadius,
centerSpaceColor,
pieTouchData,
sectionsSpace,
startDegreeOffset,
borderData,
titleSunbeamLayout,
];
}

/// Holds data related to drawing each [PieChart] section.
Expand Down Expand Up @@ -166,6 +167,7 @@ class PieChartSectionData {
String? title,
BorderSide? borderSide,
this.badgeWidget,
this.connectorLineSettings,
double? titlePositionPercentageOffset,
double? badgePositionPercentageOffset,
}) : value = value ?? 10,
Expand Down Expand Up @@ -226,6 +228,8 @@ class PieChartSectionData {
/// 1.0 means near the outside of the [PieChart].
final double badgePositionPercentageOffset;

final FLConnectorLineSettings? connectorLineSettings;

/// Copies current [PieChartSectionData] to a new [PieChartSectionData],
/// and replaces provided values.
PieChartSectionData copyWith({
Expand All @@ -252,18 +256,18 @@ class PieChartSectionData {
borderSide: borderSide ?? this.borderSide,
badgeWidget: badgeWidget ?? this.badgeWidget,
titlePositionPercentageOffset:
titlePositionPercentageOffset ?? this.titlePositionPercentageOffset,
titlePositionPercentageOffset ?? this.titlePositionPercentageOffset,
badgePositionPercentageOffset:
badgePositionPercentageOffset ?? this.badgePositionPercentageOffset,
badgePositionPercentageOffset ?? this.badgePositionPercentageOffset,
);
}

/// Lerps a [PieChartSectionData] based on [t] value, check [Tween.lerp].
static PieChartSectionData lerp(
PieChartSectionData a,
PieChartSectionData b,
double t,
) {
PieChartSectionData a,
PieChartSectionData b,
double t,
) {
return PieChartSectionData(
value: lerpDouble(a.value, b.value, t),
color: Color.lerp(a.color, b.color, t),
Expand Down Expand Up @@ -309,33 +313,28 @@ class PieTouchData extends FlTouchData<PieTouchResponse> with EquatableMixin {
MouseCursorResolver<PieTouchResponse>? mouseCursorResolver,
Duration? longPressDuration,
}) : super(
enabled ?? true,
touchCallback,
mouseCursorResolver,
longPressDuration,
);
enabled ?? true,
touchCallback,
mouseCursorResolver,
longPressDuration,
);

/// Used for equality check, see [EquatableMixin].
@override
List<Object?> get props => [
enabled,
touchCallback,
mouseCursorResolver,
longPressDuration,
];
enabled,
touchCallback,
mouseCursorResolver,
longPressDuration,
];
}

class PieTouchedSection with EquatableMixin {
/// This class Contains [touchedSection], [touchedSectionIndex] that tells
/// you touch happened on which section,
/// [touchAngle] gives you angle of touch,
/// and [touchRadius] gives you radius of the touch.
PieTouchedSection(
this.touchedSection,
this.touchedSectionIndex,
this.touchAngle,
this.touchRadius,
);
PieTouchedSection(this.touchedSection, this.touchedSectionIndex, this.touchAngle, this.touchRadius);

/// touch happened on this section
final PieChartSectionData? touchedSection;
Expand All @@ -352,11 +351,11 @@ class PieTouchedSection with EquatableMixin {
/// Used for equality check, see [EquatableMixin].
@override
List<Object?> get props => [
touchedSection,
touchedSectionIndex,
touchAngle,
touchRadius,
];
touchedSection,
touchedSectionIndex,
touchAngle,
touchRadius,
];
}

/// Holds information about touch response in the [PieChart].
Expand Down
141 changes: 115 additions & 26 deletions lib/src/chart/pie_chart/pie_chart_painter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:math' as math;
import 'package:fl_chart/fl_chart.dart';
import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart';
import 'package:fl_chart/src/chart/base/line.dart';
import 'package:fl_chart/src/chart/pie_chart/pie_chart_connector_lines.dart';
import 'package:fl_chart/src/chart/pie_chart/pie_chart_data.dart';
import 'package:fl_chart/src/extensions/paint_extension.dart';
import 'package:fl_chart/src/utils/canvas_wrapper.dart';
Expand Down Expand Up @@ -345,11 +346,11 @@ class PieChartPainter extends BaseChartPainter<PieChartData> {
/// - badge widget positions
@visibleForTesting
void drawTexts(
BuildContext context,
CanvasWrapper canvasWrapper,
PaintHolder<PieChartData> holder,
double centerRadius,
) {
BuildContext context,
CanvasWrapper canvasWrapper,
PaintHolder<PieChartData> holder,
double centerRadius,
) {
final data = holder.data;
final viewSize = canvasWrapper.size;
final center = Offset(viewSize.width / 2, viewSize.height / 2);
Expand All @@ -362,27 +363,33 @@ class PieChartPainter extends BaseChartPainter<PieChartData> {
final sweepAngle = 360 * (section.value / data.sumValue);
final sectionCenterAngle = startAngle + (sweepAngle / 2);

double? rotateAngle;
if (data.titleSunbeamLayout) {
if (sectionCenterAngle >= 90 && sectionCenterAngle <= 270) {
rotateAngle = sectionCenterAngle - 180;
} else {
rotateAngle = sectionCenterAngle;
}
}

Offset sectionCenter(double percentageOffset) =>
center +
Offset(
math.cos(Utils().radians(sectionCenterAngle)) *
(centerRadius + (section.radius * percentageOffset)),
math.sin(Utils().radians(sectionCenterAngle)) *
(centerRadius + (section.radius * percentageOffset)),
);

Offset(
math.cos(Utils().radians(sectionCenterAngle)) *
(centerRadius + (section.radius * percentageOffset)),
math.sin(Utils().radians(sectionCenterAngle)) *
(centerRadius + (section.radius * percentageOffset)),
);

// Calculate the center offset for the title (label) position
final sectionCenterOffsetTitle =
sectionCenter(section.titlePositionPercentageOffset);
sectionCenter(section.titlePositionPercentageOffset);

// Draw the connector line and get the end position
Offset endPosition = sectionCenterOffsetTitle;
if (section.connectorLineSettings != null) {
endPosition = _drawConnectorLine(
canvasWrapper,
sectionCenterOffsetTitle,
section.connectorLineSettings!,
sectionCenterAngle,
center,
centerRadius + section.radius,
);
}

// Adjust the text position to align with the end of the connector line
if (section.showTitle) {
final span = TextSpan(
style: Utils().getThemeAwareTextStyle(context, section.titleStyle),
Expand All @@ -395,17 +402,99 @@ class PieChartPainter extends BaseChartPainter<PieChartData> {
textScaler: holder.textScaler,
)..layout();

canvasWrapper.drawText(
tp,
sectionCenterOffsetTitle - Offset(tp.width / 2, tp.height / 2),
rotateAngle,
// Calculate the angle to rotate the text based on the connector line's angle
final double angleAdjustment = math.atan2(
endPosition.dy - sectionCenterOffsetTitle.dy,
endPosition.dx - sectionCenterOffsetTitle.dx,
);

// Calculate the offset based on the angle and position the text above the connector line
final Offset adjustedTextPosition = endPosition +
Offset(
-tp.width / 3,
-tp.height / 2,
).translate(
math.cos(angleAdjustment) * 11, // Increased from 5 to 8 for a slight move away
math.sin(angleAdjustment) * 11, // Increased from 5 to 8 for a slight move away
);

// Draw the text at the calculated position
canvasWrapper.drawText(tp, adjustedTextPosition);
}

tempAngle += sweepAngle;
}
}

Offset _drawConnectorLine(
CanvasWrapper canvas,
Offset start,
FLConnectorLineSettings settings,
double sectionCenterAngle,
Offset center,
double radius,
) {
// Adjust the start position to be slightly closer to the outer edge
final double adjustedRadius = radius - 0.5;

// Calculate the start position on the adjusted outer edge of the pie chart
final Offset startPosition = center + Offset(
math.cos(Utils().radians(sectionCenterAngle)) * adjustedRadius,
math.sin(Utils().radians(sectionCenterAngle)) * adjustedRadius,
);

// Define the curve offset length
final double curveLength = 10.0;

// Calculate the first point where the line bends (extends outward)
final Offset curvePosition = startPosition + Offset(
math.cos(Utils().radians(sectionCenterAngle)) * curveLength,
math.sin(Utils().radians(sectionCenterAngle)) * curveLength,
);

// Determine the direction for the second line based on the section's position
final bool isLeftSide = sectionCenterAngle > 90 && sectionCenterAngle < 270;

// Adjust the angle slightly for segments near the edges to ensure the line points outward
final double endAngleAdjustment = isLeftSide ? -30.0 : 30.0;

// Handle both percentage-based and fixed length values
final double lineLength = (settings.length is String)
? _parseLength(settings.length as String)
: (settings.length is double ? settings.length as double : 15.0);

// Calculate the end position for the label connector based on the parsed length
final Offset endPosition = curvePosition + Offset(
math.cos(Utils().radians(sectionCenterAngle + endAngleAdjustment)) * lineLength,
math.sin(Utils().radians(sectionCenterAngle + endAngleAdjustment)) * lineLength,
);

final paint = Paint()
..color = settings.color ?? Colors.black
..strokeWidth = settings.width
..style = PaintingStyle.stroke;

// Draw the first part of the line (straight outward)
canvas.drawLine(startPosition, curvePosition, paint);

// Draw the second part of the line (angled toward the label)
canvas.drawLine(curvePosition, endPosition, paint);

return endPosition;
}

// Helper method to parse percentage-based length strings like '10%', '20%' etc.
double _parseLength(String length) {
if (length.endsWith('%')) {
final percentageValue = double.tryParse(length.replaceAll('%', ''));
if (percentageValue != null) {
return (percentageValue / 100) * 30.0; // Scale this multiplier as needed
}
}
// Default to a fixed value if parsing fails
return 15.0;
}

/// Calculates center radius based on the provided sections radius
@visibleForTesting
double calculateCenterRadius(
Expand Down
Loading