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
2 changes: 2 additions & 0 deletions example/lib/presentation/samples/chart_samples.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import 'pie/pie_chart_sample1.dart';
import 'pie/pie_chart_sample2.dart';
import 'pie/pie_chart_sample3.dart';
import 'pie/pie_chart_sample4.dart';
import 'pie/pie_chart_sample5.dart';
import 'radar/radar_chart_sample1.dart';
import 'scatter/scatter_chart_sample1.dart';
import 'scatter/scatter_chart_sample2.dart';
Expand Down Expand Up @@ -63,6 +64,7 @@ class ChartSamples {
PieChartSample(2, (context) => const PieChartSample2()),
PieChartSample(3, (context) => const PieChartSample3()),
PieChartSample(4, (context) => const PieChartSample4()),
PieChartSample(5, (context) => const PieChartSample5()),
],
ChartType.scatter: [
ScatterChartSample(1, (context) => ScatterChartSample1()),
Expand Down
93 changes: 93 additions & 0 deletions example/lib/presentation/samples/pie/pie_chart_sample5.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:fl_chart_app/presentation/resources/app_colors.dart';
import 'package:flutter/material.dart';

class PieChartSample5 extends StatefulWidget {
const PieChartSample5({super.key});

@override
State<PieChartSample5> createState() => _PieChartSample5State();
}

class _PieChartSample5State extends State<PieChartSample5> {
int touchedIndex = -1;

@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 1.3,
child: AspectRatio(
aspectRatio: 1,
child: PieChart(
PieChartData(
pieTouchData: PieTouchData(
touchCallback: (FlTouchEvent event, pieTouchResponse) {
setState(() {
if (!event.isInterestedForInteractions ||
pieTouchResponse == null ||
pieTouchResponse.touchedSection == null) {
touchedIndex = -1;
return;
}
touchedIndex =
pieTouchResponse.touchedSection!.touchedSectionIndex;
});
},
),
borderData: FlBorderData(show: false),
sectionsSpace: 3,
centerSpaceRadius: 24,
sections: showingSections(),
),
),
),
);
}

static const _blueDotPattern = DotPattern(
color: Color(0x59ffffff), // white at ~35% opacity
spacing: 8,
dotRadius: 1.5,
);

List<PieChartSectionData> showingSections() {
return List.generate(4, (i) {
final isTouched = i == touchedIndex;
final radius = 56.0;
final fontSize = isTouched ? 18.0 : 14.0;
const shadows = [Shadow(color: Colors.black, blurRadius: 2)];

// Even indices (0, 2): blue section with dots + purple segment
// Odd indices (1, 3): purple section + blue segment with dots
final isBlueSection = i.isEven;

return PieChartSectionData(
color: isBlueSection
? AppColors.contentColorBlue
: AppColors.contentColorPurple,
value: 20,
title: '20%',
radius: radius,
titleStyle: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.bold,
color: AppColors.contentColorWhite,
shadows: shadows,
),
pattern: isBlueSection ? _blueDotPattern : const DotPattern.disabled(),
segments: [
PieChartStackSegmentData(
fromRadius: radius * 0.6,
toRadius: radius,
// Fully opaque so section dots don't bleed through
color: isBlueSection
? AppColors.contentColorPurple
: AppColors.contentColorBlue,
pattern:
isBlueSection ? const DotPattern.disabled() : _blueDotPattern,
),
],
);
});
}
}
1 change: 1 addition & 0 deletions lib/fl_chart.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ export 'src/chart/radar_chart/radar_chart.dart';
export 'src/chart/radar_chart/radar_chart_data.dart';
export 'src/chart/scatter_chart/scatter_chart.dart';
export 'src/chart/scatter_chart/scatter_chart_data.dart';
export 'src/utils/patterns/dot_pattern.dart';
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 @@ -166,6 +166,7 @@ class PieChartSectionData with EquatableMixin {
double? titlePositionPercentageOffset,
double? badgePositionPercentageOffset,
List<PieChartStackSegmentData>? segments,
DotPattern? pattern,
}) : value = value ?? 10,
color = color ?? Colors.cyan,
radius = (radius ?? 40).clamp(0, double.infinity).toDouble(),
Expand All @@ -176,7 +177,8 @@ class PieChartSectionData with EquatableMixin {
(cornerRadius ?? 0.0).clamp(0, double.infinity).toDouble(),
titlePositionPercentageOffset = titlePositionPercentageOffset ?? 0.5,
badgePositionPercentageOffset = badgePositionPercentageOffset ?? 0.5,
segments = segments ?? const [];
segments = segments ?? const [],
pattern = pattern ?? const DotPattern.disabled();

/// It determines how much space it should occupy around the circle.
///
Expand Down Expand Up @@ -237,6 +239,9 @@ class PieChartSectionData with EquatableMixin {
/// the section's [radius]. Values are clamped to [0, radius] at render time.
final List<PieChartStackSegmentData> segments;

/// Optional dot pattern overlay drawn over the main section fill.
final DotPattern pattern;

/// Copies current [PieChartSectionData] to a new [PieChartSectionData],
/// and replaces provided values.
PieChartSectionData copyWith({
Expand All @@ -253,6 +258,7 @@ class PieChartSectionData with EquatableMixin {
double? titlePositionPercentageOffset,
double? badgePositionPercentageOffset,
List<PieChartStackSegmentData>? segments,
DotPattern? pattern,
}) =>
PieChartSectionData(
value: value ?? this.value,
Expand All @@ -270,6 +276,7 @@ class PieChartSectionData with EquatableMixin {
badgePositionPercentageOffset:
badgePositionPercentageOffset ?? this.badgePositionPercentageOffset,
segments: segments ?? this.segments,
pattern: pattern ?? this.pattern,
);

/// Lerps a [PieChartSectionData] based on [t] value, check [Tween.lerp].
Expand Down Expand Up @@ -304,6 +311,7 @@ class PieChartSectionData with EquatableMixin {
b.segments,
t,
),
pattern: DotPattern.lerp(a.pattern, b.pattern, t),
);

/// Used for equality check, see [EquatableMixin].
Expand All @@ -322,6 +330,7 @@ class PieChartSectionData with EquatableMixin {
titlePositionPercentageOffset,
badgePositionPercentageOffset,
segments,
pattern,
];
}

Expand Down Expand Up @@ -359,8 +368,10 @@ class PieChartStackSegmentData with EquatableMixin {
required this.toRadius,
Color? color,
this.gradient,
DotPattern? pattern,
}) : fromRadius = fromRadius ?? 0,
color = color ?? Colors.purple;
color = color ?? Colors.purple,
pattern = pattern ?? const DotPattern.disabled();

/// The start radius of this segment (distance from center of the section).
/// Clamped to [0, sectionRadius] at render time.
Expand All @@ -376,19 +387,24 @@ class PieChartStackSegmentData with EquatableMixin {
/// Defines the gradient of segment. If specified, overrides the color setting.
final Gradient? gradient;

/// Optional dot pattern overlay for accessibility and contrast.
final DotPattern pattern;

/// Copies current [PieChartStackSegmentData] to a new [PieChartStackSegmentData],
/// and replaces provided values.
PieChartStackSegmentData copyWith({
double? fromRadius,
double? toRadius,
Color? color,
Gradient? gradient,
DotPattern? pattern,
}) =>
PieChartStackSegmentData(
fromRadius: fromRadius ?? this.fromRadius,
toRadius: toRadius ?? this.toRadius,
color: color ?? this.color,
gradient: gradient ?? this.gradient,
pattern: pattern ?? this.pattern,
);

/// Lerps a [PieChartStackSegmentData] based on [t] value, check [Tween.lerp].
Expand All @@ -402,6 +418,7 @@ class PieChartStackSegmentData with EquatableMixin {
toRadius: lerpDouble(a.toRadius, b.toRadius, t)!,
color: lerpColor(a.color, b.color, t),
gradient: Gradient.lerp(a.gradient, b.gradient, t),
pattern: DotPattern.lerp(a.pattern, b.pattern, t),
);

/// Used for equality check, see [EquatableMixin].
Expand All @@ -411,6 +428,7 @@ class PieChartStackSegmentData with EquatableMixin {
toRadius,
color,
gradient,
pattern,
];
}

Expand Down
56 changes: 56 additions & 0 deletions lib/src/chart/pie_chart/pie_chart_painter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,10 @@ class PieChartPainter extends BaseChartPainter<PieChartData> {
..style = PaintingStyle.fill;
canvasWrapper.drawPath(mainPath, _sectionPaint);

if (section.pattern.enabled) {
_drawDotPatternOverPath(canvasWrapper, mainPath, section.pattern);
}

for (final seg in section.segments) {
final clampedFrom = seg.fromRadius.clamp(0.0, section.radius);
final clampedTo = seg.toRadius.clamp(0.0, section.radius);
Expand All @@ -235,6 +239,7 @@ class PieChartPainter extends BaseChartPainter<PieChartData> {
Path segmentPath,
CanvasWrapper canvasWrapper,
) {
// Fill base (color/gradient)
_sectionPaint
..setColorOrGradient(
segment.color,
Expand All @@ -243,6 +248,57 @@ class PieChartPainter extends BaseChartPainter<PieChartData> {
)
..style = PaintingStyle.fill;
canvasWrapper.drawPath(segmentPath, _sectionPaint);

// Optional accessibility dot pattern overlay
if (segment.pattern.enabled) {
_drawDotPatternOverPath(
canvasWrapper,
segmentPath,
segment.pattern,
);
}
}

void _drawDotPatternOverPath(
CanvasWrapper canvasWrapper,
Path clipPath,
DotPattern pattern,
) {
canvasWrapper
..save()
..clipPath(clipPath);

final bounds = clipPath.getBounds();
if (bounds.isEmpty) {
canvasWrapper.restore();
return;
}

final spacing = pattern.spacing <= 0 ? 6.0 : pattern.spacing;
final dotRadius = pattern.dotRadius <= 0 ? 1.5 : pattern.dotRadius;

final paint = Paint()
..color = pattern.color
..style = PaintingStyle.fill
..isAntiAlias = true;

// Use a grid that's stable relative to the segment bounds to avoid flicker
// when sections are offset. The pattern phase allows fine-tuning the grid
// position.
final startX = bounds.left -
((bounds.left - pattern.phase.dx) % spacing) +
pattern.phase.dx;
final startY = bounds.top -
((bounds.top - pattern.phase.dy) % spacing) +
pattern.phase.dy;

for (var y = startY; y <= bounds.bottom + spacing; y += spacing) {
for (var x = startX; x <= bounds.right + spacing; x += spacing) {
canvasWrapper.drawCircle(Offset(x, y), dotRadius, paint);
}
}

canvasWrapper.restore();
}

@visibleForTesting
Expand Down
73 changes: 73 additions & 0 deletions lib/src/utils/patterns/dot_pattern.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import 'dart:ui';

import 'package:flutter/material.dart';

/// Simple dot pattern configuration for improved accessibility and contrast.
///
/// When enabled, dots are drawn over the segment using a stable, world-aligned
/// grid, clipped to the segment path to avoid flickering on repaints.
class DotPattern {
const DotPattern({
required Color color,
double spacing = 6,
double dotRadius = 1.5,
Offset phase = Offset.zero,
}) : this._(
enabled: true,
color: color,
spacing: spacing,
dotRadius: dotRadius,
phase: phase,
);

const DotPattern._({
required this.enabled,
required this.color,
required this.spacing,
required this.dotRadius,
this.phase = Offset.zero,
});

const DotPattern.disabled()
: this._(
enabled: false,
color: Colors.transparent,
spacing: 0,
dotRadius: 0,
phase: Offset.zero,
);

final bool enabled;
final Color color;
final double spacing;
final double dotRadius;
final Offset phase;

static DotPattern lerp(DotPattern a, DotPattern b, double t) {
return DotPattern(
color: Color.lerp(a.color, b.color, t) ?? a.color,
spacing: lerpDouble(a.spacing, b.spacing, t)!,
dotRadius: lerpDouble(a.dotRadius, b.dotRadius, t)!,
phase: Offset(
lerpDouble(a.phase.dx, b.phase.dx, t)!,
lerpDouble(a.phase.dy, b.phase.dy, t)!,
),
);
}

DotPattern copyWith({
bool? enabled,
Color? color,
double? spacing,
double? dotRadius,
Offset? phase,
}) {
if (enabled == false) return const DotPattern.disabled();
return DotPattern(
color: color ?? this.color,
spacing: spacing ?? this.spacing,
dotRadius: dotRadius ?? this.dotRadius,
phase: phase ?? this.phase,
);
}
}
Loading