From f652121694c6994058254a7b68a3a37c769f0ece Mon Sep 17 00:00:00 2001 From: imaNNeoFighT Date: Sat, 11 Jan 2025 00:57:48 +0100 Subject: [PATCH] Implement error indicator in bar chart. (sample 8) --- .../samples/bar/bar_chart_sample8.dart | 37 +++++++++- lib/src/chart/bar_chart/bar_chart_data.dart | 44 +++++++++++ .../chart/bar_chart/bar_chart_painter.dart | 73 +++++++++++++++++++ 3 files changed, 150 insertions(+), 4 deletions(-) diff --git a/example/lib/presentation/samples/bar/bar_chart_sample8.dart b/example/lib/presentation/samples/bar/bar_chart_sample8.dart index 044b68070..247e307f1 100644 --- a/example/lib/presentation/samples/bar/bar_chart_sample8.dart +++ b/example/lib/presentation/samples/bar/bar_chart_sample8.dart @@ -61,12 +61,14 @@ class BarChartSample1State extends State { BarChartGroupData makeGroupData( int x, double y, + FlErrorRange errorRange, ) { return BarChartGroupData( x: x, barRods: [ BarChartRodData( toY: y, + toYErrorRange: errorRange, color: x >= 4 ? Colors.transparent : widget.barColor, borderRadius: BorderRadius.zero, borderDashArray: x >= 4 ? [4, 4] : null, @@ -134,12 +136,39 @@ class BarChartSample1State extends State { ), barGroups: List.generate( 7, - (i) => makeGroupData( - i, - Random().nextInt(290).toDouble() + 10, - ), + (i) { + final y = Random().nextInt(290).toDouble() + 10; + final lowerBy = y < 50 + ? Random().nextDouble() * 10 + : Random().nextDouble() * 30 + 5; + final upperBy = y > 290 + ? Random().nextDouble() * 10 + : Random().nextDouble() * 30 + 5; + return makeGroupData( + i, + y, + FlErrorRange( + lowerBy: lowerBy, + upperBy: upperBy, + ), + ); + }, ), gridData: const FlGridData(show: false), + errorIndicatorData: FlErrorIndicatorData( + painter: _errorPainter, + ), ); } + + FlSpotErrorRangePainter _errorPainter( + BarChartSpotErrorRangeCallbackInput input, + ) => + FlSimpleErrorPainter( + lineWidth: 2.0, + capLength: 14, + lineColor: input.groupIndex < 4 + ? AppColors.contentColorOrange + : AppColors.primary.withValues(alpha: 0.5), + ); } diff --git a/lib/src/chart/bar_chart/bar_chart_data.dart b/lib/src/chart/bar_chart/bar_chart_data.dart index eadf35ae4..ca8b3edd6 100644 --- a/lib/src/chart/bar_chart/bar_chart_data.dart +++ b/lib/src/chart/bar_chart/bar_chart_data.dart @@ -48,6 +48,7 @@ class BarChartData extends AxisChartData with EquatableMixin { super.backgroundColor, ExtraLinesData? extraLinesData, super.rotationQuarterTurns, + this.errorIndicatorData = const FlErrorIndicatorData(), }) : barGroups = barGroups ?? const [], groupsSpace = groupsSpace ?? 16, alignment = alignment ?? BarChartAlignment.spaceEvenly, @@ -79,6 +80,10 @@ class BarChartData extends AxisChartData with EquatableMixin { /// Handles touch behaviors and responses. final BarTouchData barTouchData; + /// Holds data for showing error indicators on the spots in this line. + final FlErrorIndicatorData + errorIndicatorData; + /// Copies current [BarChartData] to a new [BarChartData], /// and replaces provided values. BarChartData copyWith({ @@ -96,6 +101,8 @@ class BarChartData extends AxisChartData with EquatableMixin { Color? backgroundColor, ExtraLinesData? extraLinesData, int? rotationQuarterTurns, + FlErrorIndicatorData? + errorIndicatorData, }) => BarChartData( barGroups: barGroups ?? this.barGroups, @@ -112,6 +119,7 @@ class BarChartData extends AxisChartData with EquatableMixin { backgroundColor: backgroundColor ?? this.backgroundColor, extraLinesData: extraLinesData ?? this.extraLinesData, rotationQuarterTurns: rotationQuarterTurns ?? this.rotationQuarterTurns, + errorIndicatorData: errorIndicatorData ?? this.errorIndicatorData, ); /// Lerps a [BaseChartData] based on [t] value, check [Tween.lerp]. @@ -135,6 +143,11 @@ class BarChartData extends AxisChartData with EquatableMixin { extraLinesData: ExtraLinesData.lerp(a.extraLinesData, b.extraLinesData, t), rotationQuarterTurns: b.rotationQuarterTurns, + errorIndicatorData: FlErrorIndicatorData.lerp( + a.errorIndicatorData, + b.errorIndicatorData, + t, + ), ); } else { throw Exception('Illegal State'); @@ -158,6 +171,7 @@ class BarChartData extends AxisChartData with EquatableMixin { backgroundColor, extraLinesData, rotationQuarterTurns, + errorIndicatorData, ]; } @@ -318,6 +332,7 @@ class BarChartRodData with EquatableMixin { BarChartRodData({ double? fromY, required this.toY, + this.toYErrorRange, Color? color, this.gradient, double? width, @@ -341,6 +356,8 @@ class BarChartRodData with EquatableMixin { /// [BarChart] renders rods vertically from [fromY] to [toY]. final double toY; + final FlErrorRange? toYErrorRange; + /// If provided, this [BarChartRodData] draws with this [color] /// Otherwise we use [gradient] to draw the background. /// It throws an exception if you provide both [color] and [gradient] @@ -380,6 +397,7 @@ class BarChartRodData with EquatableMixin { BarChartRodData copyWith({ double? fromY, double? toY, + FlErrorRange? toYErrorRange, Color? color, Gradient? gradient, double? width, @@ -392,6 +410,7 @@ class BarChartRodData with EquatableMixin { BarChartRodData( fromY: fromY ?? this.fromY, toY: toY ?? this.toY, + toYErrorRange: toYErrorRange ?? this.toYErrorRange, color: color ?? this.color, gradient: gradient ?? this.gradient, width: width ?? this.width, @@ -413,6 +432,7 @@ class BarChartRodData with EquatableMixin { borderSide: BorderSide.lerp(a.borderSide, b.borderSide, t), fromY: lerpDouble(a.fromY, b.fromY, t), toY: lerpDouble(a.toY, b.toY, t)!, + toYErrorRange: FlErrorRange.lerp(a.toYErrorRange, b.toYErrorRange, t), backDrawRodData: BackgroundBarChartRodData.lerp( a.backDrawRodData, b.backDrawRodData, @@ -427,6 +447,7 @@ class BarChartRodData with EquatableMixin { List get props => [ fromY, toY, + toYErrorRange, width, borderRadius, borderDashArray, @@ -929,6 +950,29 @@ class BarTouchedSpot extends TouchedSpot with EquatableMixin { ]; } +class BarChartSpotErrorRangeCallbackInput + extends FlSpotErrorRangeCallbackInput { + BarChartSpotErrorRangeCallbackInput({ + required this.group, + required this.groupIndex, + required this.rod, + required this.barRodIndex, + }); + + final BarChartGroupData group; + final int groupIndex; + final BarChartRodData rod; + final int barRodIndex; + + @override + List get props => [ + group, + groupIndex, + rod, + barRodIndex, + ]; +} + /// It lerps a [BarChartData] to another [BarChartData] (handles animation for updating values) class BarChartDataTween extends Tween { BarChartDataTween({required BarChartData begin, required BarChartData end}) diff --git a/lib/src/chart/bar_chart/bar_chart_painter.dart b/lib/src/chart/bar_chart/bar_chart_painter.dart index 1a2e8dbe8..92c9e0945 100644 --- a/lib/src/chart/bar_chart/bar_chart_painter.dart +++ b/lib/src/chart/bar_chart/bar_chart_painter.dart @@ -37,6 +37,7 @@ class BarChartPainter extends AxisChartPainter { _clipPaint = Paint(); } + late Paint _barPaint; late Paint _barStrokePaint; late Paint _bgTouchTooltipPaint; @@ -93,6 +94,8 @@ class BarChartPainter extends AxisChartPainter { drawBars(canvasWrapper, _groupBarsPosition!, holder); + drawErrorIndicatorData(canvasWrapper, _groupBarsPosition!, holder); + if (data.extraLinesData.extraLinesOnTop) { super.drawHorizontalLines( context, @@ -352,6 +355,75 @@ class BarChartPainter extends AxisChartPainter { } } + @visibleForTesting + void drawErrorIndicatorData( + CanvasWrapper canvasWrapper, + List groupBarsPosition, + PaintHolder holder, + ) { + final data = holder.data; + final errorIndicatorData = data.errorIndicatorData; + if (!errorIndicatorData.show) { + return; + } + + 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]; + + if (barRod.toYErrorRange == null) { + continue; + } + + final x = groupBarsPosition[i].barsX[j]; + + final y = getPixelY(barRod.toY, viewSize, holder); + final top = getPixelY( + barRod.toY + barRod.toYErrorRange!.upperBy, + viewSize, + holder, + ) - + y; + + final bottom = getPixelY( + barRod.toY - barRod.toYErrorRange!.lowerBy, + viewSize, + holder, + ) - + y; + + final relativeErrorPixelsRect = Rect.fromLTRB( + 0, + top, + 0, + bottom, + ); + + final painter = errorIndicatorData.painter( + BarChartSpotErrorRangeCallbackInput( + group: barGroup, + groupIndex: i, + rod: barRod, + barRodIndex: j, + ), + ); + canvasWrapper.drawErrorIndicator( + painter, + FlSpot( + barGroup.x.toDouble(), + barRod.toY, + yError: barRod.toYErrorRange, + ), + Offset(x, y), + relativeErrorPixelsRect, + holder.data, + ); + } + } + } + @visibleForTesting void drawTouchTooltip( BuildContext context, @@ -764,6 +836,7 @@ class BarChartPainter extends AxisChartPainter { @visibleForTesting class GroupBarsPosition { GroupBarsPosition(this.groupX, this.barsX); + final double groupX; final List barsX; }