diff --git a/lib/src/chart/bar_chart/bar_chart_data.dart b/lib/src/chart/bar_chart/bar_chart_data.dart index b99cdaf2a..0579f91fa 100644 --- a/lib/src/chart/bar_chart/bar_chart_data.dart +++ b/lib/src/chart/bar_chart/bar_chart_data.dart @@ -299,6 +299,92 @@ class BarChartGroupData with EquatableMixin { ]; } +class BarChartRodBadge with EquatableMixin { + const BarChartRodBadge({ + required this.text, + this.color = const Color(0xFF006837), + this.textStyle = const TextStyle( + color: Color(0xFFFFFFFF), + fontSize: 11, + fontWeight: FontWeight.bold, + ), + this.radius = 12, + this.margin = 4, + this.points = 12, + this.innerRadiusRatio = 0.8, + this.scale = 1, + }); + + final String text; + + final Color color; + + final TextStyle textStyle; + + final double radius; + + final double margin; + + final int points; + + final double innerRadiusRatio; + + final double scale; + + BarChartRodBadge copyWith({ + String? text, + Color? color, + TextStyle? textStyle, + double? radius, + double? margin, + int? points, + double? innerRadiusRatio, + double? scale, + }) => + BarChartRodBadge( + text: text ?? this.text, + color: color ?? this.color, + textStyle: textStyle ?? this.textStyle, + radius: radius ?? this.radius, + margin: margin ?? this.margin, + points: points ?? this.points, + innerRadiusRatio: innerRadiusRatio ?? this.innerRadiusRatio, + scale: scale ?? this.scale, + ); + + static BarChartRodBadge? lerp( + BarChartRodBadge? a, + BarChartRodBadge? b, + double t, + ) { + if (a == null || b == null) { + return b; + } + return BarChartRodBadge( + text: b.text, + color: Color.lerp(a.color, b.color, t)!, + textStyle: TextStyle.lerp(a.textStyle, b.textStyle, t)!, + radius: lerpDouble(a.radius, b.radius, t)!, + margin: lerpDouble(a.margin, b.margin, t)!, + points: b.points, + innerRadiusRatio: lerpDouble(a.innerRadiusRatio, b.innerRadiusRatio, t)!, + scale: lerpDouble(a.scale, b.scale, t)!, + ); + } + + @override + List get props => [ + text, + color, + textStyle, + radius, + margin, + points, + innerRadiusRatio, + scale, + ]; +} + /// Holds data about rendering each rod (or bar) in the [BarChart]. class BarChartRodData with EquatableMixin { /// [BarChart] renders rods vertically from zero to [toY], @@ -341,6 +427,7 @@ class BarChartRodData with EquatableMixin { BackgroundBarChartRodData? backDrawRodData, List? rodStackItems, this.label = const BarChartRodLabel(show: false), + this.badge, }) : fromY = fromY ?? 0, color = color ?? ((color == null && gradient == null) ? Colors.cyan : null), @@ -399,6 +486,8 @@ class BarChartRodData with EquatableMixin { /// Optional label to display near the rod tip. final BarChartRodLabel label; + final BarChartRodBadge? badge; + /// Determines the upward or downward direction bool isUpward() => toY >= fromY; @@ -417,6 +506,7 @@ class BarChartRodData with EquatableMixin { BackgroundBarChartRodData? backDrawRodData, List? rodStackItems, BarChartRodLabel? label, + BarChartRodBadge? badge, }) => BarChartRodData( fromY: fromY ?? this.fromY, @@ -431,6 +521,7 @@ class BarChartRodData with EquatableMixin { backDrawRodData: backDrawRodData ?? this.backDrawRodData, rodStackItems: rodStackItems ?? this.rodStackItems, label: label ?? this.label, + badge: badge ?? this.badge, ); /// Lerps a [BarChartRodData] based on [t] value, check [Tween.lerp]. @@ -453,6 +544,7 @@ class BarChartRodData with EquatableMixin { rodStackItems: lerpBarChartRodStackList(a.rodStackItems, b.rodStackItems, t), label: BarChartRodLabel.lerpBarChartRodLabel(a.label, b.label, t), + badge: BarChartRodBadge.lerp(a.badge, b.badge, t), ); /// Used for equality check, see [EquatableMixin]. @@ -470,6 +562,7 @@ class BarChartRodData with EquatableMixin { color, gradient, label, + badge, ]; } diff --git a/lib/src/chart/bar_chart/bar_chart_painter.dart b/lib/src/chart/bar_chart/bar_chart_painter.dart index 3dac6feca..f85ac188a 100644 --- a/lib/src/chart/bar_chart/bar_chart_painter.dart +++ b/lib/src/chart/bar_chart/bar_chart_painter.dart @@ -94,6 +94,7 @@ class BarChartPainter extends AxisChartPainter { drawBars(canvasWrapper, _groupBarsPosition!, holder); drawBarLabels(context, canvasWrapper, _groupBarsPosition!, holder); + drawRodBadges(canvasWrapper, _groupBarsPosition!, holder); drawErrorIndicatorData(canvasWrapper, _groupBarsPosition!, holder); @@ -416,6 +417,89 @@ class BarChartPainter extends AxisChartPainter { } } + @visibleForTesting + void drawRodBadges( + CanvasWrapper canvasWrapper, + List groupBarsPosition, + PaintHolder holder, + ) { + final data = holder.data; + final viewSize = canvasWrapper.size; + + for (var i = 0; i < data.barGroups.length; i++) { + final barGroup = data.barGroups[i]; + for (var j = 0; j < barGroup.barRods.length; j++) { + final barRod = barGroup.barRods[j]; + final badge = barRod.badge; + if (badge == null || barRod.toY == barRod.fromY) { + continue; + } + + final x = groupBarsPosition[i].barsX[j]; + final tipY = getPixelY(barRod.toY, viewSize, holder); + final center = Offset(x, tipY - badge.margin - badge.radius); + _drawSealBadge(canvasWrapper, center, badge, holder); + } + } + } + + void _drawSealBadge( + CanvasWrapper canvasWrapper, + Offset center, + BarChartRodBadge badge, + PaintHolder holder, + ) { + final scale = badge.scale <= 0 ? 0.0 : badge.scale; + if (scale <= 0.01) { + return; + } + final opacity = scale > 1 ? 1.0 : scale; + + final radius = badge.radius * scale; + final innerRadius = radius * badge.innerRadiusRatio; + final angleStep = pi / badge.points; + final path = Path(); + for (var k = 0; k < badge.points * 2; k++) { + final r = k.isEven ? radius : innerRadius; + final angle = k * angleStep - pi / 2; + final px = center.dx + r * cos(angle); + final py = center.dy + r * sin(angle); + if (k == 0) { + path.moveTo(px, py); + } else { + path.lineTo(px, py); + } + } + path.close(); + + canvasWrapper.drawPath( + path, + Paint() + ..color = badge.color.withValues(alpha: badge.color.a * opacity) + ..style = PaintingStyle.fill, + ); + + final baseStyle = badge.textStyle; + final baseColor = baseStyle.color ?? const Color(0xFFFFFFFF); + final textStyle = baseStyle.copyWith( + fontSize: (baseStyle.fontSize ?? 11) * scale, + color: baseColor.withValues(alpha: baseColor.a * opacity), + ); + final textPainter = TextPainter( + text: TextSpan(text: badge.text, style: textStyle), + textAlign: TextAlign.center, + textDirection: TextDirection.ltr, + textScaler: holder.textScaler, + )..layout(); + textPainter.paint( + canvasWrapper.canvas, + Offset( + center.dx - textPainter.width / 2, + center.dy - textPainter.height / 2, + ), + ); + } + @visibleForTesting void drawBarLabels( BuildContext context, diff --git a/lib/src/chart/base/axis_chart/axis_chart_scaffold_widget.dart b/lib/src/chart/base/axis_chart/axis_chart_scaffold_widget.dart index 2687958f4..465fb4124 100644 --- a/lib/src/chart/base/axis_chart/axis_chart_scaffold_widget.dart +++ b/lib/src/chart/base/axis_chart/axis_chart_scaffold_widget.dart @@ -205,6 +205,10 @@ class _AxisChartScaffoldWidgetState extends State { ? widget.data.borderData.border : null; + final borderRadius = widget.data.borderData.isVisible() + ? widget.data.borderData.borderRadius + : null; + final borderWidth = borderData == null ? 0 : borderData.dimensions.horizontal; final borderHeight = @@ -251,7 +255,8 @@ class _AxisChartScaffoldWidgetState extends State { final widgets = [ Container( margin: margin, - decoration: BoxDecoration(border: borderData), + decoration: + BoxDecoration(border: borderData, borderRadius: borderRadius), child: child, ), ]; diff --git a/lib/src/chart/base/base_chart/base_chart_data.dart b/lib/src/chart/base/base_chart/base_chart_data.dart index 95b1fe28a..7790f3d59 100644 --- a/lib/src/chart/base/base_chart/base_chart_data.dart +++ b/lib/src/chart/base/base_chart/base_chart_data.dart @@ -38,10 +38,12 @@ class FlBorderData with EquatableMixin { FlBorderData({ bool? show, Border? border, + this.borderRadius, }) : show = show ?? true, border = border ?? Border.all(); final bool show; final Border border; + final BorderRadius? borderRadius; /// returns false if all borders have 0 width or 0 opacity bool isVisible() => show && border.isVisible(); @@ -51,6 +53,7 @@ class FlBorderData with EquatableMixin { FlBorderData( show: b.show, border: Border.lerp(a.border, b.border, t), + borderRadius: BorderRadius.lerp(a.borderRadius, b.borderRadius, t), ); /// Copies current [FlBorderData] to a new [FlBorderData], @@ -58,10 +61,12 @@ class FlBorderData with EquatableMixin { FlBorderData copyWith({ bool? show, Border? border, + BorderRadius? borderRadius, }) => FlBorderData( show: show ?? this.show, border: border ?? this.border, + borderRadius: borderRadius ?? this.borderRadius, ); /// Used for equality check, see [EquatableMixin]. @@ -69,6 +74,7 @@ class FlBorderData with EquatableMixin { List get props => [ show, border, + borderRadius, ]; }