Skip to content

Commit

Permalink
Add the ability to select the RadarChart shape (circle or polygon)
Browse files Browse the repository at this point in the history
  • Loading branch information
FlorianArnould authored and imaNNeo committed Jun 10, 2022
1 parent 2de36af commit 9542241
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 6 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## newVersion
* **BUGFIX** (by @imaNNeoFighT): Fix tooltip issue on negative bar charts, #978.
* **IMPROVEMENT** (by @imaNNeoFighT): Use Container to draw axis-based charts border.
* **FEATURE** (by @FlorianArnould) Add the ability to select the RadarChart shape (circle or polygon)

## 0.51.0
* **FEATURE** (by @imaNNeoFighT): Add `SideTitleWidget` to help you use it in [SideTitles.getTitlesWidget]. It's a wrapper around your widget. It keeps your provided `child` widget close to the chart. It has `angle` and `space` properties to handle margin and rotation. There is a `axisSide` property that you should fill, it has provided to you in the MetaData object. Check the below sample:
Expand Down
14 changes: 14 additions & 0 deletions lib/src/chart/radar_chart/radar_chart_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import 'package:fl_chart/src/chart/radar_chart/radar_extension.dart';

typedef GetTitleByIndexFunction = String Function(int index);

enum RadarShape {
circle,
polygon,
}

/// [RadarChart] needs this class to render itself.
///
/// It holds data needed to draw a radar chart,
Expand All @@ -23,6 +28,9 @@ class RadarChartData extends BaseChartData with EquatableMixin {
/// [radarBorderData] is used to draw [RadarChart] border
final BorderSide radarBorderData;

/// [radarShape] is used to draw [RadarChart] border and background
final RadarShape radarShape;

/// [getTitle] is used to draw titles outside the [RadarChart]
/// [getTitle] is type of [GetTitleByIndexFunction] so you should return a valid [String]
/// for each [index]
Expand Down Expand Up @@ -119,6 +127,7 @@ class RadarChartData extends BaseChartData with EquatableMixin {
@required List<RadarDataSet>? dataSets,
Color? radarBackgroundColor,
BorderSide? radarBorderData,
RadarShape? radarShape,
GetTitleByIndexFunction? getTitle,
TextStyle? titleTextStyle,
double? titlePositionPercentageOffset,
Expand All @@ -141,6 +150,7 @@ class RadarChartData extends BaseChartData with EquatableMixin {
radarBackgroundColor = radarBackgroundColor ?? Colors.transparent,
radarBorderData =
radarBorderData ?? const BorderSide(color: Colors.black, width: 2),
radarShape = radarShape ?? RadarShape.circle,
radarTouchData = radarTouchData ?? RadarTouchData(),
getTitle = getTitle,
titleTextStyle = titleTextStyle,
Expand All @@ -161,6 +171,7 @@ class RadarChartData extends BaseChartData with EquatableMixin {
List<RadarDataSet>? dataSets,
Color? radarBackgroundColor,
BorderSide? radarBorderData,
RadarShape? radarShape,
GetTitleByIndexFunction? getTitle,
TextStyle? titleTextStyle,
double? titlePositionPercentageOffset,
Expand All @@ -175,6 +186,7 @@ class RadarChartData extends BaseChartData with EquatableMixin {
dataSets: dataSets ?? this.dataSets,
radarBackgroundColor: radarBackgroundColor ?? this.radarBackgroundColor,
radarBorderData: radarBorderData ?? this.radarBorderData,
radarShape: radarShape ?? this.radarShape,
getTitle: getTitle ?? this.getTitle,
titleTextStyle: titleTextStyle ?? this.titleTextStyle,
titlePositionPercentageOffset:
Expand Down Expand Up @@ -207,6 +219,7 @@ class RadarChartData extends BaseChartData with EquatableMixin {
gridBorderData: BorderSide.lerp(a.gridBorderData, b.gridBorderData, t),
radarBorderData:
BorderSide.lerp(a.radarBorderData, b.radarBorderData, t),
radarShape: b.radarShape,
tickBorderData: BorderSide.lerp(a.tickBorderData, b.tickBorderData, t),
borderData: FlBorderData.lerp(a.borderData, b.borderData, t),
radarTouchData: b.radarTouchData,
Expand All @@ -224,6 +237,7 @@ class RadarChartData extends BaseChartData with EquatableMixin {
dataSets,
radarBackgroundColor,
radarBorderData,
radarShape,
getTitle,
titleTextStyle,
titlePositionPercentageOffset,
Expand Down
44 changes: 38 additions & 6 deletions lib/src/chart/radar_chart/radar_chart_painter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,26 @@ class RadarChartPainter extends BaseChartPainter<RadarChartData> {

_backgroundPaint.color = data.radarBackgroundColor;

/// draw radar background
canvasWrapper.drawCircle(centerOffset, radius, _backgroundPaint);

_borderPaint
..color = data.radarBorderData.color
..strokeWidth = data.radarBorderData.width;

/// draw radar border
canvasWrapper.drawCircle(centerOffset, radius, _borderPaint);
if (data.radarShape == RadarShape.circle) {
/// draw radar background
canvasWrapper.drawCircle(centerOffset, radius, _backgroundPaint);

/// draw radar border
canvasWrapper.drawCircle(centerOffset, radius, _borderPaint);
} else {
final path =
_generatePolygonPath(centerX, centerY, radius, data.titleCount);

/// draw radar background
canvasWrapper.drawPath(path, _backgroundPaint);

/// draw radar border
canvasWrapper.drawPath(path, _borderPaint);
}

final dataSetMaxValue = data.maxEntry.value;
final dataSetMinValue = data.minEntry.value;
Expand All @@ -107,8 +118,15 @@ class RadarChartPainter extends BaseChartPainter<RadarChartData> {
ticks.sublist(0, ticks.length - 1).asMap().forEach(
(index, tick) {
final tickRadius = tickDistance * (index + 1);
if (data.radarShape == RadarShape.circle) {
canvasWrapper.drawCircle(centerOffset, tickRadius, _tickPaint);
} else {
canvasWrapper.drawPath(
_generatePolygonPath(centerX, centerY, tickRadius, data.titleCount),
_tickPaint,
);
}

canvasWrapper.drawCircle(centerOffset, tickRadius, _tickPaint);
_ticksTextPaint
..text = TextSpan(
text: tick.toStringAsFixed(1),
Expand All @@ -124,6 +142,20 @@ class RadarChartPainter extends BaseChartPainter<RadarChartData> {
);
}

Path _generatePolygonPath(
double centerX, double centerY, double radius, int count) {
final path = Path();
path.moveTo(centerX, centerY - radius);
final angle = (2 * pi) / count;
for (var index = 0; index < count; index++) {
final xAngle = cos(angle * index - pi / 2);
final yAngle = sin(angle * index - pi / 2);
path.lineTo(centerX + radius * xAngle, centerY + radius * yAngle);
}
path.lineTo(centerX, centerY - radius);
return path;
}

void drawGrids(
CanvasWrapper canvasWrapper, PaintHolder<RadarChartData> holder) {
final data = holder.data;
Expand Down
1 change: 1 addition & 0 deletions repo_files/documentations/radar_chart.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ When you change the chart's state, it animates to the new state internally (usin
|:---------------|:---------------|:-------|
|dataSets| list of [RadarDataSet ](#RadarDataSet) that is shown on the radar chart|[]|
|radarBackgroundColor| This property fills the background of the radar with the specified color.| Colors.transparent|
|radarShape| the shape of the border and background |RadarShape.circle|
|radarBorderData| shows a border for radar chart|BorderSide(color: Colors.black, width: 2)|
|getTitle| This function helps the radar chart to draw titles outside the chart.|null|
|titleTextStyle|TextStyle of the titles|TextStyle(color: Colors.black, fontSize: 12)|
Expand Down
7 changes: 7 additions & 0 deletions test/chart/radar_chart/radar_chart_data_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ void main() {
)),
false);

expect(
radarChartData1 ==
radarChartData1Clone.copyWith(
radarShape: RadarShape.polygon,
),
false);

expect(
radarChartData1 ==
radarChartData1Clone.copyWith(radarTouchData: radarTouchData2),
Expand Down
79 changes: 79 additions & 0 deletions test/chart/radar_chart/radar_chart_painter_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,85 @@ void main() {
expect((tp.text as TextSpan).style, MockData.textStyle1);
expect(result.captured[1] as Offset, const Offset(205, 76));
});

test('test 2', () {
const viewSize = Size(400, 300);

final RadarChartData data = RadarChartData(
dataSets: [
RadarDataSet(dataEntries: [
const RadarEntry(value: 1),
const RadarEntry(value: 2),
const RadarEntry(value: 3),
]),
RadarDataSet(dataEntries: [
const RadarEntry(value: 3),
const RadarEntry(value: 1),
const RadarEntry(value: 2),
]),
RadarDataSet(dataEntries: [
const RadarEntry(value: 2),
const RadarEntry(value: 3),
const RadarEntry(value: 1),
]),
],
radarBorderData: const BorderSide(color: MockData.color6, width: 33),
radarShape: RadarShape.polygon,
tickBorderData: const BorderSide(color: MockData.color5, width: 55),
radarBackgroundColor: MockData.color2,
);

final RadarChartPainter radarChartPainter = RadarChartPainter();
final holder = PaintHolder<RadarChartData>(data, data, 1.0);

final mockCanvasWrapper = MockCanvasWrapper();
when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize);
when(mockCanvasWrapper.canvas).thenReturn(MockCanvas());

final mockUtils = MockUtils();
when(mockUtils.getThemeAwareTextStyle(any, any))
.thenReturn(MockData.textStyle1);
Utils.changeInstance(mockUtils);

MockBuildContext mockContext = MockBuildContext();

List<Map<String, dynamic>> drawPathResult = [];
when(mockCanvasWrapper.drawPath(captureAny, captureAny))
.thenAnswer((inv) {
drawPathResult.add({
'path': inv.positionalArguments[0] as Path,
'paint_color': (inv.positionalArguments[1] as Paint).color,
'paint_stroke': (inv.positionalArguments[1] as Paint).strokeWidth,
'paint_style': (inv.positionalArguments[1] as Paint).style,
});
});

radarChartPainter.drawTicks(mockContext, mockCanvasWrapper, holder);

expect(drawPathResult.length, 3);

// Background circle
expect(drawPathResult[0]['paint_color'], MockData.color2);
expect(drawPathResult[0]['paint_stroke'], 0);
expect(drawPathResult[0]['paint_style'], PaintingStyle.fill);

// Border circle
expect(drawPathResult[1]['paint_color'], MockData.color6);
expect(drawPathResult[1]['paint_stroke'], 33);
expect(drawPathResult[1]['paint_style'], PaintingStyle.stroke);

// First Tick
expect(drawPathResult[2]['paint_color'], MockData.color5);
expect(drawPathResult[2]['paint_stroke'], 55);
expect(drawPathResult[2]['paint_style'], PaintingStyle.stroke);

final result = verify(mockCanvasWrapper.drawText(captureAny, captureAny));
expect(result.callCount, 1);
final tp = result.captured[0] as TextPainter;
expect((tp.text as TextSpan).text, '1.0');
expect((tp.text as TextSpan).style, MockData.textStyle1);
expect(result.captured[1] as Offset, const Offset(205, 76));
});
});

group('drawGrids()', () {
Expand Down

0 comments on commit 9542241

Please sign in to comment.