Repository: imaNNeo/fl_chart (dart)

Issue:
Add feature to add dashed border around a bar
Please enable to add a feature to add dashed border to a bar in the barchart. This can useful when the user need to present a predicted data in the future. Below is a sample graph that I took from an app.

![3-mint-ios (2)](https://user-images.githubusercontent.com/28390211/189798755-3eeb4ab6-c8f0-4058-afa1-d5d2cbd040b8.png)

Pull request:
Add optional `borderDashArray` to `BarChartRodData`  and utilize `toDashedPath` with bar border.

Web:
![Näyttökuva 2023-10-16 183207](https://github.com/imaNNeo/fl_chart/assets/48412269/f5d9c50d-92bf-4efa-aa19-5469c759afc1)

Android:
![Screenshot_1697470672](https://github.com/imaNNeo/fl_chart/assets/48412269/2f3e6b26-444d-4b56-9ff8-f8baad067948)

Closes #1144

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 19473ca33..c1d4a0fc9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
 * **BUGFIX** (by @imaNNeo) Fix bar line shadow crash when we have only one (or zero) spot, #1466
 * **BUGFIX** (by @imaNNeo) Fix having negative `toY` (or positive `fromY`) in BarChart's `minY` and `maxY` calculations, #1470
 * **BUGFIX** (by @bobatsar) Fix bars drawn outside of diagram
+* **FEATURE** (by @k0psutin) Add dashed border to BarChartRodData, #1144
 
 ## 0.64.0
 * **BUGFIX** (by @Anas35) Fix Tooltip not displaying when value from BackgroundBarChartRodData is less than zero. #1345.
diff --git a/example/lib/presentation/samples/bar/bar_chart_sample8.dart b/example/lib/presentation/samples/bar/bar_chart_sample8.dart
new file mode 100644
index 000000000..2198fb072
--- /dev/null
+++ b/example/lib/presentation/samples/bar/bar_chart_sample8.dart
@@ -0,0 +1,145 @@
+import 'dart:math';
+
+import 'package:fl_chart_app/presentation/resources/app_resources.dart';
+import 'package:fl_chart_app/util/extensions/color_extensions.dart';
+import 'package:fl_chart/fl_chart.dart';
+import 'package:flutter/material.dart';
+
+class BarChartSample8 extends StatefulWidget {
+  BarChartSample8({super.key});
+
+  final Color barBackgroundColor =
+      AppColors.contentColorWhite.darken().withOpacity(0.3);
+  final Color barColor = AppColors.contentColorWhite;
+
+  @override
+  State<StatefulWidget> createState() => BarChartSample1State();
+}
+
+class BarChartSample1State extends State<BarChartSample8> {
+  @override
+  Widget build(BuildContext context) {
+    return AspectRatio(
+      aspectRatio: 1,
+      child: Padding(
+        padding: const EdgeInsets.all(16),
+        child: Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+          children: <Widget>[
+            Row(
+              mainAxisAlignment: MainAxisAlignment.center,
+              children: [
+                const Icon(Icons.graphic_eq),
+                const SizedBox(
+                  width: 32,
+                ),
+                Text(
+                  'Sales forecasting chart',
+                  style: TextStyle(
+                    color: widget.barColor,
+                    fontSize: 18,
+                    fontWeight: FontWeight.bold,
+                  ),
+                ),
+              ],
+            ),
+            const SizedBox(
+              height: 32,
+            ),
+            Expanded(
+              child: BarChart(
+                randomData(),
+              ),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+
+  BarChartGroupData makeGroupData(
+    int x,
+    double y,
+  ) {
+    return BarChartGroupData(
+      x: x,
+      barRods: [
+        BarChartRodData(
+          toY: y,
+          color: x >= 4 ? Colors.transparent : widget.barColor,
+          borderRadius: BorderRadius.zero,
+          borderDashArray: x >= 4 ? [4, 4] : null,
+          width: 22,
+          borderSide: BorderSide(color: widget.barColor, width: 2.0),
+        ),
+      ],
+    );
+  }
+
+  Widget getTitles(double value, TitleMeta meta) {
+    const style = TextStyle(
+      color: Colors.white,
+      fontWeight: FontWeight.bold,
+      fontSize: 14,
+    );
+    List<String> days = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];
+
+    Widget text = Text(
+      days[value.toInt()],
+      style: style,
+    );
+
+    return SideTitleWidget(
+      axisSide: meta.axisSide,
+      space: 16,
+      child: text,
+    );
+  }
+
+  BarChartData randomData() {
+    return BarChartData(
+      maxY: 300.0,
+      barTouchData: BarTouchData(
+        enabled: false,
+      ),
+      titlesData: FlTitlesData(
+        show: true,
+        bottomTitles: AxisTitles(
+          sideTitles: SideTitles(
+            showTitles: true,
+            getTitlesWidget: getTitles,
+            reservedSize: 38,
+          ),
+        ),
+        leftTitles: const AxisTitles(
+          sideTitles: SideTitles(
+            reservedSize: 30,
+            showTitles: true,
+          ),
+        ),
+        topTitles: const AxisTitles(
+          sideTitles: SideTitles(
+            showTitles: false,
+          ),
+        ),
+        rightTitles: const AxisTitles(
+          sideTitles: SideTitles(
+            showTitles: false,
+          ),
+        ),
+      ),
+      borderData: FlBorderData(
+        show: false,
+      ),
+      barGroups: List.generate(
+        7,
+        (i) => makeGroupData(
+          i,
+          Random().nextInt(290).toDouble() + 10,
+        ),
+      ),
+      gridData: const FlGridData(show: false),
+    );
+  }
+}
diff --git a/example/lib/presentation/samples/chart_samples.dart b/example/lib/presentation/samples/chart_samples.dart
index 4114cd5f6..98e090148 100644
--- a/example/lib/presentation/samples/chart_samples.dart
+++ b/example/lib/presentation/samples/chart_samples.dart
@@ -7,6 +7,7 @@ import 'bar/bar_chart_sample4.dart';
 import 'bar/bar_chart_sample5.dart';
 import 'bar/bar_chart_sample6.dart';
 import 'bar/bar_chart_sample7.dart';
+import 'bar/bar_chart_sample8.dart';
 import 'chart_sample.dart';
 import 'line/line_chart_sample1.dart';
 import 'line/line_chart_sample10.dart';
@@ -49,6 +50,7 @@ class ChartSamples {
       BarChartSample(5, (context) => const BarChartSample5()),
       BarChartSample(6, (context) => const BarChartSample6()),
       BarChartSample(7, (context) => BarChartSample7()),
+      BarChartSample(8, (context) => BarChartSample8()),
     ],
     ChartType.pie: [
       PieChartSample(1, (context) => const PieChartSample1()),
diff --git a/lib/src/chart/bar_chart/bar_chart_data.dart b/lib/src/chart/bar_chart/bar_chart_data.dart
index 6292faf92..c0207de66 100644
--- a/lib/src/chart/bar_chart/bar_chart_data.dart
+++ b/lib/src/chart/bar_chart/bar_chart_data.dart
@@ -290,6 +290,7 @@ class BarChartRodData with EquatableMixin {
   /// and the x is equivalent to the [BarChartGroupData.x] value.
   ///
   /// It renders each rod using [color], [width], and [borderRadius] for rounding corners and also [borderSide] for stroke border.
+  /// Optionally you can use [borderDashArray] if you want your borders to have dashed lines.
   ///
   /// This bar draws with provided [color] or [gradient].
   /// You must provide one of them.
@@ -319,6 +320,7 @@ class BarChartRodData with EquatableMixin {
     this.gradient,
     double? width,
     BorderRadius? borderRadius,
+    this.borderDashArray,
     BorderSide? borderSide,
     BackgroundBarChartRodData? backDrawRodData,
     List<BarChartRodStackItem>? rodStackItems,
@@ -353,6 +355,9 @@ class BarChartRodData with EquatableMixin {
   /// If you want to have a rounded rod, set this value.
   final BorderRadius? borderRadius;
 
+  /// If you want to have dashed border, set this value.
+  final List<int>? borderDashArray;
+
   /// If you want to have a border for rod, set this value.
   final BorderSide borderSide;
 
@@ -377,6 +382,7 @@ class BarChartRodData with EquatableMixin {
     Gradient? gradient,
     double? width,
     BorderRadius? borderRadius,
+    List<int>? dashArray,
     BorderSide? borderSide,
     BackgroundBarChartRodData? backDrawRodData,
     List<BarChartRodStackItem>? rodStackItems,
@@ -388,6 +394,7 @@ class BarChartRodData with EquatableMixin {
       gradient: gradient ?? this.gradient,
       width: width ?? this.width,
       borderRadius: borderRadius ?? this.borderRadius,
+      borderDashArray: borderDashArray,
       borderSide: borderSide ?? this.borderSide,
       backDrawRodData: backDrawRodData ?? this.backDrawRodData,
       rodStackItems: rodStackItems ?? this.rodStackItems,
@@ -402,6 +409,7 @@ class BarChartRodData with EquatableMixin {
       color: Color.lerp(a.color, b.color, t),
       width: lerpDouble(a.width, b.width, t),
       borderRadius: BorderRadius.lerp(a.borderRadius, b.borderRadius, t),
+      borderDashArray: lerpIntList(a.borderDashArray, b.borderDashArray, t),
       borderSide: BorderSide.lerp(a.borderSide, b.borderSide, t),
       fromY: lerpDouble(a.fromY, b.fromY, t),
       toY: lerpDouble(a.toY, b.toY, t)!,
@@ -422,6 +430,7 @@ class BarChartRodData with EquatableMixin {
         toY,
         width,
         borderRadius,
+        borderDashArray,
         borderSide,
         backDrawRodData,
         rodStackItems,
diff --git a/lib/src/chart/bar_chart/bar_chart_painter.dart b/lib/src/chart/bar_chart/bar_chart_painter.dart
index ab99f7cbd..0214682fa 100644
--- a/lib/src/chart/bar_chart/bar_chart_painter.dart
+++ b/lib/src/chart/bar_chart/bar_chart_painter.dart
@@ -6,6 +6,7 @@ import 'package:fl_chart/src/chart/base/axis_chart/axis_chart_painter.dart';
 import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart';
 import 'package:fl_chart/src/extensions/bar_chart_data_extension.dart';
 import 'package:fl_chart/src/extensions/paint_extension.dart';
+import 'package:fl_chart/src/extensions/path_extension.dart';
 import 'package:fl_chart/src/extensions/rrect_extension.dart';
 import 'package:fl_chart/src/utils/canvas_wrapper.dart';
 import 'package:fl_chart/src/utils/utils.dart';
@@ -277,14 +278,6 @@ class BarChartPainter extends AxisChartPainter<BarChartData> {
           );
           canvasWrapper.drawRRect(barRRect, _barPaint);
 
-          // draw border stroke
-          if (borderSide.width > 0 && borderSide.color.opacity > 0) {
-            _barStrokePaint
-              ..color = borderSide.color
-              ..strokeWidth = borderSide.width;
-            canvasWrapper.drawRRect(barRRect, _barStrokePaint);
-          }
-
           // draw rod stack
           if (barRod.rodStackItems.isNotEmpty) {
             for (var i = 0; i < barRod.rodStackItems.length; i++) {
@@ -316,6 +309,22 @@ class BarChartPainter extends AxisChartPainter<BarChartData> {
               );
             }
           }
+
+          // draw border stroke
+          if (borderSide.width > 0 && borderSide.color.opacity > 0) {
+            _barStrokePaint
+              ..color = borderSide.color
+              ..strokeWidth = borderSide.width;
+
+            final borderPath = Path()..addRRect(barRRect);
+
+            canvasWrapper.drawPath(
+              borderPath.toDashedPath(
+                barRod.borderDashArray,
+              ),
+              _barStrokePaint,
+            );
+          }
         }
       }
     }
diff --git a/repo_files/documentations/bar_chart.md b/repo_files/documentations/bar_chart.md
index 9c28a16c1..178c2f212 100644
--- a/repo_files/documentations/bar_chart.md
+++ b/repo_files/documentations/bar_chart.md
@@ -57,6 +57,7 @@ enum values {`start`, `end`, `center`, `spaceEvenly`, `spaceAround`, `spaceBetwe
 |gradient| You can use any [Gradient](https://api.flutter.dev/flutter/dart-ui/Gradient-class.html) here. such as [LinearGradient](https://api.flutter.dev/flutter/painting/LinearGradient-class.html) or [RadialGradient](https://api.flutter.dev/flutter/painting/RadialGradient-class.html)|null|
 |width|stroke width of the rod bar|8|
 |borderRadius|Determines the edge rounding of the bar corners, see [BorderRadius](https://api.flutter.dev/flutter/painting/BorderRadius-class.html). When `null`, it defaults to completely round bars. |null|
+|borderDashArray|Determines wether the border stroke is dashed. It is a circular array of dash offsets and lengths. For example, the array `[5, 10]` would result in dashes 5 pixels long followed by blank spaces 10 pixels long.  The array `[5, 10, 5]` would result in a 5 pixel dash, a 10 pixel gap, a 5 pixel dash, a 5 pixel gap, a 10 pixel dash, etc.|null|
 |borderSide|Determines the border stroke around of the bar, see [BorderSide](https://api.flutter.dev/flutter/painting/BorderSide-class.html). When `null`, it defaults to draw no stroke. |null|
 |backDrawRodData|if provided, draws a rod in the background of the line bar, check the [BackgroundBarChartRodData](#BackgroundBarChartRodData)|null|
 |rodStackItem|if you want to have stacked bar chart, provide a list of [BarChartRodStackItem](#BarChartRodStackItem), it will draw over your rod.|[]|


Test patch:
diff --git a/test/chart/bar_chart/bar_chart_painter_test.dart b/test/chart/bar_chart/bar_chart_painter_test.dart
index 44877dbe5..ef7684efb 100644
--- a/test/chart/bar_chart/bar_chart_painter_test.dart
+++ b/test/chart/bar_chart/bar_chart_painter_test.dart
@@ -2,6 +2,7 @@ import 'package:fl_chart/fl_chart.dart';
 import 'package:fl_chart/src/chart/bar_chart/bar_chart_painter.dart';
 import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart';
 import 'package:fl_chart/src/extensions/bar_chart_data_extension.dart';
+import 'package:fl_chart/src/extensions/path_extension.dart';
 import 'package:fl_chart/src/utils/canvas_wrapper.dart';
 import 'package:fl_chart/src/utils/utils.dart';
 import 'package:flutter/material.dart';
@@ -787,6 +788,205 @@ void main() {
       expect(results[3]['paint_color'], const Color(0x33333333));
       expect(results[4]['paint_color'], const Color(0x44444444));
     });
+
+    test('test 4', () {
+      const viewSize = Size(200, 100);
+
+      final barGroups = [
+        BarChartGroupData(
+          x: 0,
+          barRods: [
+            BarChartRodData(
+              fromY: 0,
+              toY: 5,
+              borderRadius: BorderRadius.zero,
+              color: Colors.white,
+            ),
+          ],
+        ),
+        BarChartGroupData(
+          x: 1,
+          barRods: [
+            BarChartRodData(
+              fromY: 0,
+              toY: 10,
+              borderRadius: BorderRadius.zero,
+              color: Colors.white,
+            ),
+          ],
+        ),
+        BarChartGroupData(
+          x: 3,
+          barRods: [
+            BarChartRodData(
+              fromY: 0,
+              toY: 15,
+              borderDashArray: [4, 4],
+              borderSide: const BorderSide(
+                color: Colors.white,
+                width: 2,
+              ),
+              borderRadius: BorderRadius.zero,
+              color: Colors.transparent,
+            ),
+          ],
+        ),
+      ];
+
+      final data = BarChartData(barGroups: barGroups);
+
+      final barChartPainter = BarChartPainter();
+      final holder =
+          PaintHolder<BarChartData>(data, data, TextScaler.noScaling);
+
+      final mockCanvasWrapper = MockCanvasWrapper();
+      when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize);
+      when(mockCanvasWrapper.canvas).thenReturn(MockCanvas());
+
+      final groupsX = data.calculateGroupsX(viewSize.width);
+      final barGroupsPosition = barChartPainter.calculateGroupAndBarsPosition(
+        viewSize,
+        groupsX,
+        barGroups,
+      );
+
+      final rodDataResults = <Map<String, dynamic>>[];
+      final borderResult = <Map<String, dynamic>>[];
+
+      when(mockCanvasWrapper.drawRRect(captureAny, captureAny))
+          .thenAnswer((inv) {
+        final rrect = inv.positionalArguments[0] as RRect;
+        final paint = inv.positionalArguments[1] as Paint;
+        rodDataResults.add({
+          'rrect': rrect,
+          'paint_color': paint.color,
+        });
+      });
+
+      when(mockCanvasWrapper.drawPath(captureAny, captureAny))
+          .thenAnswer((inv) {
+        final path = inv.positionalArguments[0] as Path;
+        final paint = inv.positionalArguments[1] as Paint;
+        borderResult.add({
+          'path': path,
+          'paint_color': paint.color,
+        });
+      });
+
+      barChartPainter.drawBars(mockCanvasWrapper, barGroupsPosition, holder);
+      expect(rodDataResults.length, 3);
+      expect(rodDataResults[0]['paint_color'], Colors.white);
+      expect(rodDataResults[1]['paint_color'], Colors.white);
+      expect(rodDataResults[2]['paint_color'], Colors.transparent);
+
+      expect(borderResult.length, 1);
+      expect(borderResult[0]['paint_color'], Colors.white);
+      final rrect = rodDataResults[2]['rrect'] as RRect;
+      final path = Path()..addRRect(rrect);
+      final expectedPath = path.toDashedPath([4, 4]);
+      final currentPath = borderResult[0]['path'] as Path;
+
+      expect(HelperMethods.equalsPaths(expectedPath, currentPath), true);
+    });
+
+    test('test 5', () {
+      const viewSize = Size(200, 100);
+
+      final barGroups = [
+        BarChartGroupData(
+          x: 0,
+          barRods: [
+            BarChartRodData(
+              fromY: 0,
+              toY: 5,
+              borderRadius: BorderRadius.zero,
+              color: Colors.white,
+            ),
+          ],
+        ),
+        BarChartGroupData(
+          x: 1,
+          barRods: [
+            BarChartRodData(
+              fromY: 0,
+              toY: 10,
+              borderRadius: BorderRadius.zero,
+              color: Colors.white,
+            ),
+          ],
+        ),
+        BarChartGroupData(
+          x: 3,
+          barRods: [
+            BarChartRodData(
+              fromY: 0,
+              toY: 15,
+              borderSide: const BorderSide(
+                color: Colors.white,
+                width: 2,
+              ),
+              borderRadius: BorderRadius.zero,
+              color: Colors.transparent,
+            ),
+          ],
+        ),
+      ];
+
+      final data = BarChartData(barGroups: barGroups);
+
+      final barChartPainter = BarChartPainter();
+      final holder =
+          PaintHolder<BarChartData>(data, data, TextScaler.noScaling);
+
+      final mockCanvasWrapper = MockCanvasWrapper();
+      when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize);
+      when(mockCanvasWrapper.canvas).thenReturn(MockCanvas());
+
+      final groupsX = data.calculateGroupsX(viewSize.width);
+      final barGroupsPosition = barChartPainter.calculateGroupAndBarsPosition(
+        viewSize,
+        groupsX,
+        barGroups,
+      );
+
+      final rodDataResults = <Map<String, dynamic>>[];
+      final borderResult = <Map<String, dynamic>>[];
+
+      when(mockCanvasWrapper.drawRRect(captureAny, captureAny))
+          .thenAnswer((inv) {
+        final rrect = inv.positionalArguments[0] as RRect;
+        final paint = inv.positionalArguments[1] as Paint;
+        rodDataResults.add({
+          'rrect': rrect,
+          'paint_color': paint.color,
+        });
+      });
+
+      when(mockCanvasWrapper.drawPath(captureAny, captureAny))
+          .thenAnswer((inv) {
+        final path = inv.positionalArguments[0] as Path;
+        final paint = inv.positionalArguments[1] as Paint;
+        borderResult.add({
+          'path': path,
+          'paint_color': paint.color,
+        });
+      });
+
+      barChartPainter.drawBars(mockCanvasWrapper, barGroupsPosition, holder);
+      expect(rodDataResults.length, 3);
+      expect(rodDataResults[0]['paint_color'], Colors.white);
+      expect(rodDataResults[1]['paint_color'], Colors.white);
+      expect(rodDataResults[2]['paint_color'], Colors.transparent);
+
+      expect(borderResult.length, 1);
+      expect(borderResult[0]['paint_color'], Colors.white);
+      final rrect = rodDataResults[2]['rrect'] as RRect;
+      final path = Path()..addRRect(rrect);
+      final expectedPath = path.toDashedPath(null);
+      final currentPath = borderResult[0]['path'] as Path;
+
+      expect(HelperMethods.equalsPaths(expectedPath, currentPath), true);
+    });
   });
 
   group('drawTouchTooltip()', () {