From 37f21b120650eaa2c8f92f89329c20a0c6081d54 Mon Sep 17 00:00:00 2001 From: Florian ARNOULD Date: Mon, 20 Jun 2022 21:54:47 +0200 Subject: [PATCH 1/8] feat: gauge diagram --- lib/fl_chart.dart | 2 + lib/src/chart/gauge_chart/gauge_chart.dart | 61 +++++ .../chart/gauge_chart/gauge_chart_data.dart | 247 ++++++++++++++++++ .../gauge_chart/gauge_chart_painter.dart | 143 ++++++++++ .../gauge_chart/gauge_chart_renderer.dart | 100 +++++++ 5 files changed, 553 insertions(+) create mode 100644 lib/src/chart/gauge_chart/gauge_chart.dart create mode 100644 lib/src/chart/gauge_chart/gauge_chart_data.dart create mode 100644 lib/src/chart/gauge_chart/gauge_chart_painter.dart create mode 100644 lib/src/chart/gauge_chart/gauge_chart_renderer.dart diff --git a/lib/fl_chart.dart b/lib/fl_chart.dart index d3597fb81..233a92955 100644 --- a/lib/fl_chart.dart +++ b/lib/fl_chart.dart @@ -15,3 +15,5 @@ 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/chart/gauge_chart/gauge_chart.dart'; +export 'src/chart/gauge_chart/gauge_chart_data.dart'; diff --git a/lib/src/chart/gauge_chart/gauge_chart.dart b/lib/src/chart/gauge_chart/gauge_chart.dart new file mode 100644 index 000000000..d50b82b82 --- /dev/null +++ b/lib/src/chart/gauge_chart/gauge_chart.dart @@ -0,0 +1,61 @@ +import 'package:fl_chart/src/chart/gauge_chart/gauge_chart_data.dart'; +import 'package:fl_chart/src/chart/gauge_chart/gauge_chart_renderer.dart'; +import 'package:flutter/cupertino.dart'; + +class GaugeChart extends ImplicitlyAnimatedWidget { + /// Determines how the [GaugeChartData] should be look like. + final GaugeChartData data; + + /// We pass this key to our renderers which are supposed to + /// render the chart itself (without anything around the chart). + final Key? chartRendererKey; + + /// [data] determines how the [GaugeChart] should be look like, + /// when you make any change in the [GaugeChartData], it updates + /// new values with animation, and duration is [swapAnimationDuration]. + /// also you can change the [swapAnimationCurve] + /// which default is [Curves.linear]. + const GaugeChart( + this.data, { + this.chartRendererKey, + Key? key, + Duration swapAnimationDuration = const Duration(milliseconds: 150), + Curve swapAnimationCurve = Curves.linear, + }) : super( + key: key, + duration: swapAnimationDuration, + curve: swapAnimationCurve); + + /// Creates a [_GaugeChartState] + @override + _GaugeChartState createState() => _GaugeChartState(); +} + +class _GaugeChartState extends AnimatedWidgetBaseState { + /// we handle under the hood animations (implicit animations) via this tween, + /// it lerps between the old [GaugeChartData] to the new one. + GaugeChartDataTween? _gaugeChartDataTween; + + @override + Widget build(BuildContext context) { + final showingData = _getData(); + + return GaugeChartLeaf( + data: _gaugeChartDataTween!.evaluate(animation), + targetData: showingData, + ); + } + + GaugeChartData _getData() { + return widget.data; + } + + @override + void forEachTween(visitor) { + _gaugeChartDataTween = visitor( + _gaugeChartDataTween, + widget.data, + (dynamic value) => GaugeChartDataTween(begin: value, end: widget.data), + ) as GaugeChartDataTween; + } +} \ No newline at end of file diff --git a/lib/src/chart/gauge_chart/gauge_chart_data.dart b/lib/src/chart/gauge_chart/gauge_chart_data.dart new file mode 100644 index 000000000..792bc59a9 --- /dev/null +++ b/lib/src/chart/gauge_chart/gauge_chart_data.dart @@ -0,0 +1,247 @@ +import 'dart:ui'; + +import 'package:equatable/equatable.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/utils/lerp.dart'; +import 'package:flutter/material.dart'; + +class ColoredTick { + final double position; + final Color color; + + const ColoredTick(this.position, this.color); +} + +abstract class ColoredTicksGenerator { + Iterable getColoredTicks(); +} + +@immutable +abstract class GaugeColor { + Color getColor(double value); + + static GaugeColor lerp(GaugeColor a, GaugeColor b, double t) { + return _LerpGaugeColor(a, b, t); + } +} + +class _LerpGaugeColor implements GaugeColor, ColoredTicksGenerator { + final GaugeColor a, b; + final double t; + _LerpGaugeColor(this.a, this.b, this.t); + + @override + Color getColor(double value) { + return Color.lerp(a.getColor(value), b.getColor(value), t)!; + } + + @override + Iterable getColoredTicks() sync* { + if (a is ColoredTicksGenerator) { + for (final tick in (a as ColoredTicksGenerator).getColoredTicks()) { + yield ColoredTick(tick.position, tick.color.withOpacity(1 - t)); + } + } + if (b is ColoredTicksGenerator) { + for (final tick in (b as ColoredTicksGenerator).getColoredTicks()) { + yield ColoredTick(tick.position, tick.color.withOpacity(t)); + } + } + } +} + +@immutable +class SimpleGaugeColor implements GaugeColor { + final Color color; + const SimpleGaugeColor({required this.color}); + + @override + Color getColor(double value) => color; +} + +class VariableGaugeColor implements GaugeColor, ColoredTicksGenerator { + final List limits; + final List colors; + + VariableGaugeColor({ + required this.limits, + required this.colors, + }) : assert( + colors.length - 1 == limits.length, + 'length of limits should be equals to colors length minus one' + ) { + assert( + limits.reduce((a, b) => a < b ? 0 : 2) == 0, + 'the limits list should be sorted in ascending order', + ); + assert( + limits.first > 0 || limits.last < 1.0, + 'limits values should be in range 0, 1 (exclusive)', + ); + } + + @override + Color getColor(double value) { + for (var i = 0; i < limits.length; i++) { + if (value < limits[i]) return colors[i]; + } + return colors.last; + } + + @override + Iterable getColoredTicks() sync* { + for(var i = 0; i < limits.length; i++) { + yield ColoredTick(limits[i], colors[i + 1]); + } + } +} + +enum GaugeTickPosition { + inner, + outer, + center, +} + +@immutable +class GaugeTicks { + final int count; + final double radius; + final Color color; + final GaugeTickPosition position; + final double margin; + final bool showChangingColorTicks; + + const GaugeTicks({ + this.count = 3, + this.radius = 3.0, + required this.color, + this.position = GaugeTickPosition.outer, + this.margin = 3, + this.showChangingColorTicks = true, + }) : assert(count > 2, 'count should be >= 2'), + assert(radius > 0, 'radius should be > 0'); + + static GaugeTicks? lerp(GaugeTicks? a, GaugeTicks? b, double t) { + if (a == null || b == null) return b; + return GaugeTicks( + color: Color.lerp(a.color, b.color, t)!, + count: lerpInt(a.count, b.count, t), + margin: lerpDouble(a.margin, b.margin, t)!, + position: b.position, + radius: lerpDouble(a.radius, b.radius, t)!, + showChangingColorTicks: b.showChangingColorTicks, + ); + } +} + +class GaugeChartData extends BaseChartData with EquatableMixin { + final StrokeCap strokeCap; + final double value; + final double strokeWidth; + final GaugeColor valueColor; + final Color? backgroundColor; + final double startAngle; + final double endAngle; + final GaugeTouchData gaugeTouchData; + final GaugeTicks? ticks; + + GaugeChartData({ + this.strokeCap = StrokeCap.butt, + this.backgroundColor, + required this.valueColor, + required this.value, + required this.strokeWidth, + required this.startAngle, + required this.endAngle, + this.ticks, + FlBorderData? borderData, + GaugeTouchData? touchData, + }) : gaugeTouchData = touchData ?? GaugeTouchData(), + super( + borderData: borderData, + touchData: touchData ?? GaugeTouchData()); + + GaugeChartData copyWith({ + StrokeCap? strokeCap, + Color? backgroundColor, + GaugeColor? valueColor, + double? value, + double? strokeWidth, + double? startAngle, + double? endAngle, + GaugeTicks? ticks, + FlBorderData? borderData, + GaugeTouchData? gaugeTouchData, + }) => + GaugeChartData( + strokeCap: strokeCap ?? this.strokeCap, + backgroundColor: backgroundColor ?? this.backgroundColor, + valueColor: valueColor ?? this.valueColor, + value: value ?? this.value, + strokeWidth: strokeWidth ?? this.strokeWidth, + startAngle: startAngle ?? this.startAngle, + endAngle: endAngle ?? this.endAngle, + ticks: ticks ?? this.ticks, + borderData: borderData ?? this.borderData, + touchData: gaugeTouchData ?? this.gaugeTouchData, + ); + + + @override + GaugeChartData lerp(BaseChartData a, BaseChartData b, double t) { + if (a is GaugeChartData && b is GaugeChartData) { + return GaugeChartData( + ticks: GaugeTicks.lerp(a.ticks, b.ticks, t), + strokeCap: b.strokeCap, + backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), + valueColor: GaugeColor.lerp(a.valueColor, b.valueColor, t), + value: lerpDouble(a.value, b.value, t)!, + strokeWidth: lerpDouble(a.strokeWidth, b.strokeWidth, t)!, + startAngle: lerpDouble(a.startAngle, b.startAngle, t)!, + endAngle: lerpDouble(a.endAngle, b.endAngle, t)!, + ); + } else { + throw Exception('Illegal State'); + } + } + + @override + List get props => [ + ticks, + strokeCap, + backgroundColor, + valueColor, + value, + strokeWidth, + startAngle, + endAngle, + ]; +} + +class GaugeTouchData extends FlTouchData { + GaugeTouchData({ + bool? enabled, + BaseTouchCallback? touchCallback, + MouseCursorResolver? mouseCursorResolver + }) : super(enabled ?? true, touchCallback, mouseCursorResolver); +} + +class GaugeTouchResponse extends BaseTouchResponse { + GaugeTouchResponse(GaugeTouchedSpot? touchedSpot); +} + +class GaugeTouchedSpot extends TouchedSpot with EquatableMixin { + GaugeTouchedSpot(FlSpot spot, Offset offset) : super(spot, offset); +} + +/// It lerps a [GaugeChartData] to another [GaugeChartData] (handles animation for updating values) +class GaugeChartDataTween extends Tween { + GaugeChartDataTween({ + required GaugeChartData begin, + required GaugeChartData end, + }) : super(begin: begin, end: end); + + /// Lerps a [GaugeChartData] based on [t] value, check [Tween.lerp]. + @override + GaugeChartData lerp(double t) => begin!.lerp(begin!, end!, t); +} \ No newline at end of file diff --git a/lib/src/chart/gauge_chart/gauge_chart_painter.dart b/lib/src/chart/gauge_chart/gauge_chart_painter.dart new file mode 100644 index 000000000..7a9cdb27b --- /dev/null +++ b/lib/src/chart/gauge_chart/gauge_chart_painter.dart @@ -0,0 +1,143 @@ +import 'dart:math'; + +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; +import 'package:fl_chart/src/chart/gauge_chart/gauge_chart_data.dart'; +import 'package:fl_chart/src/utils/canvas_wrapper.dart'; +import 'package:fl_chart/src/utils/utils.dart'; +import 'package:flutter/material.dart'; + +class GaugeChartPainter extends BaseChartPainter { + late Paint _backgroundPaint, _valuePaint, _tickPaint; + + GaugeChartPainter() : super() { + _backgroundPaint = Paint() + ..isAntiAlias = true; + _valuePaint = Paint() + ..isAntiAlias = true; + _tickPaint = Paint(); + } + + @override + void paint(BuildContext context, CanvasWrapper canvasWrapper, + PaintHolder holder) { + super.paint(context, canvasWrapper, holder); + + drawValue(canvasWrapper, holder); + drawTicks(canvasWrapper, holder); + } + + @visibleForTesting + void drawTicks(CanvasWrapper canvasWrapper, + PaintHolder holder) { + final data = holder.data; + final ticks = data.ticks; + if (ticks == null) return; + + final size = canvasWrapper.size; + + final centerOffset = center(size); + final angleRange = data.endAngle - data.startAngle; + final interTickAngle = angleRange / (ticks.count - 1); + + _tickPaint.color = ticks.color; + + final radius = gaugeRadius(size); + + /// draw radar ticks + for(var i = 0; i < ticks.count; i++) { + final angle = Utils().radians(data.startAngle + interTickAngle * i); + _drawTick(canvasWrapper, centerOffset, angle, radius, ticks, data.strokeWidth); + } + + final valueColor = data.valueColor; + if (ticks.showChangingColorTicks && valueColor is ColoredTicksGenerator) { + for(final tick in (valueColor as ColoredTicksGenerator).getColoredTicks()) { + final angle = Utils().radians(data.startAngle + angleRange * tick.position); + _tickPaint.color = tick.color; + _drawTick(canvasWrapper, centerOffset, angle, radius, ticks, data.strokeWidth); + } + } + } + + _drawTick(CanvasWrapper canvasWrapper, Offset center, double angle, double radius, GaugeTicks ticks, double strokeWidth) { + double positionRadius; + switch (ticks.position) { + case GaugeTickPosition.inner: + positionRadius = radius - strokeWidth - ticks.radius - ticks.margin; + break; + case GaugeTickPosition.outer: + positionRadius = radius + ticks.radius + ticks.margin; + break; + case GaugeTickPosition.center: + positionRadius = radius - strokeWidth / 2; + break; + } + final tickX = center.dx + cos(angle) * positionRadius; + final tickY = center.dy + sin(angle) * positionRadius; + + canvasWrapper.drawCircle(Offset(tickX, tickY), ticks.radius, _tickPaint); + } + + @visibleForTesting + void drawValue(CanvasWrapper canvasWrapper, + PaintHolder holder) { + final data = holder.data; + var size = Size.square( + canvasWrapper.size.shortestSide - data.strokeWidth, + ); + final demiStroke = data.strokeWidth / 2; + var offset = Offset( + max(canvasWrapper.size.width - canvasWrapper.size.height, 0) / 2 + demiStroke, + max(canvasWrapper.size.height - canvasWrapper.size.width, 0) / 2 + demiStroke, + ); + final backgroundColor = data.backgroundColor; + + final angleRange = data.endAngle - data.startAngle; + + // for(var i = 0; i < 3; i++) { + /// Draw background if needed + if(backgroundColor != null) { + _backgroundPaint + ..color = backgroundColor + ..strokeWidth = data.strokeWidth + ..strokeCap = data.strokeCap + ..style = PaintingStyle.stroke; + canvasWrapper.drawArc( + offset & size, + Utils().radians(data.startAngle), + Utils().radians(angleRange), + false, + _backgroundPaint, + ); + } + + /// Draw value + _valuePaint + ..color = data.valueColor.getColor(data.value) + ..strokeWidth = data.strokeWidth + ..strokeCap = data.strokeCap + ..style = PaintingStyle.stroke; + canvasWrapper.drawArc( + offset & size, + Utils().radians(data.startAngle), + Utils().radians(angleRange * data.value.clamp(0, 1)), + false, + _valuePaint, + ); + + // offset = offset + Offset(data.strokeWidth + 3, data.strokeWidth + 3); + // size = Size.square(size.width - 2 * (data.strokeWidth + 3)); + // } + } + + GaugeTouchedSpot? handleTouch( + Offset touchedPoint, Size viewSize, PaintHolder holder) { + return null; + } + + @visibleForTesting + Offset center(Size size) => Offset(size.width / 2.0, size.height / 2.0); + + @visibleForTesting + double gaugeRadius(Size size) => size.shortestSide / 2; +} \ No newline at end of file diff --git a/lib/src/chart/gauge_chart/gauge_chart_renderer.dart b/lib/src/chart/gauge_chart/gauge_chart_renderer.dart new file mode 100644 index 000000000..0f389718c --- /dev/null +++ b/lib/src/chart/gauge_chart/gauge_chart_renderer.dart @@ -0,0 +1,100 @@ +import 'package:fl_chart/src/chart/base/base_chart/render_base_chart.dart'; +import 'package:fl_chart/src/chart/gauge_chart/gauge_chart_painter.dart'; +import 'package:fl_chart/src/utils/canvas_wrapper.dart'; +import 'package:flutter/widgets.dart'; + +import '../base/base_chart/base_chart_painter.dart'; +import 'gauge_chart_data.dart'; + +/// Low level GaugeChart Widget. +class GaugeChartLeaf extends LeafRenderObjectWidget { + const GaugeChartLeaf({Key? key, required this.data, required this.targetData}) + : super(key: key); + + final GaugeChartData data, targetData; + + @override + RenderGaugeChart createRenderObject(BuildContext context) => RenderGaugeChart( + context, data, targetData, MediaQuery.of(context).textScaleFactor); + + @override + void updateRenderObject(BuildContext context, RenderGaugeChart renderObject) { + renderObject + ..data = data + ..targetData = targetData + ..textScale = MediaQuery.of(context).textScaleFactor + ..buildContext = context; + } +} + +/// Renders our RadarChart, also handles hitTest. +class RenderGaugeChart extends RenderBaseChart { + RenderGaugeChart(BuildContext context, GaugeChartData data, + GaugeChartData targetData, double textScale) + : _data = data, + _targetData = targetData, + _textScale = textScale, + super(targetData.gaugeTouchData, context); + + GaugeChartData get data => _data; + GaugeChartData _data; + + set data(GaugeChartData value) { + if (_data == value) return; + _data = value; + markNeedsPaint(); + } + + GaugeChartData get targetData => _targetData; + GaugeChartData _targetData; + + set targetData(GaugeChartData value) { + if (_targetData == value) return; + _targetData = value; + super.updateBaseTouchData(_targetData.gaugeTouchData); + markNeedsPaint(); + } + + double get textScale => _textScale; + double _textScale; + + set textScale(double value) { + if (_textScale == value) return; + _textScale = value; + markNeedsPaint(); + } + + // We couldn't mock [size] property of this class, that's why we have this + @visibleForTesting + Size? mockTestSize; + + @visibleForTesting + var painter = GaugeChartPainter(); + + PaintHolder get paintHolder { + return PaintHolder(data, targetData, textScale); + } + + @override + void paint(PaintingContext context, Offset offset) { + final canvas = context.canvas; + canvas.save(); + canvas.translate(offset.dx, offset.dy); + painter.paint( + buildContext, + CanvasWrapper(canvas, mockTestSize ?? size), + paintHolder, + ); + canvas.restore(); + } + + @override + GaugeTouchResponse getResponseAtLocation(Offset localPosition) { + var touchedSpot = painter.handleTouch( + localPosition, + mockTestSize ?? size, + paintHolder, + ); + return GaugeTouchResponse(touchedSpot); + } +} \ No newline at end of file From 185812b1d9b3efd51e22af3c7bf20680b5f9ef9c Mon Sep 17 00:00:00 2001 From: Florian ARNOULD Date: Tue, 25 Jul 2023 12:32:15 +0200 Subject: [PATCH 2/8] chore: refactors and cleanups --- .../presentation/resources/app_assets.dart | 2 + .../presentation/samples/chart_sample.dart | 6 + .../presentation/samples/chart_samples.dart | 4 + .../samples/gauge/guage_chart_sample1.dart | 52 ++++++++ example/lib/util/app_helper.dart | 3 +- lib/src/chart/gauge_chart/gauge_chart.dart | 43 +++---- .../chart/gauge_chart/gauge_chart_data.dart | 115 +++++++++--------- .../gauge_chart/gauge_chart_painter.dart | 30 ++--- .../gauge_chart/gauge_chart_renderer.dart | 51 +++++--- 9 files changed, 187 insertions(+), 119 deletions(-) create mode 100644 example/lib/presentation/samples/gauge/guage_chart_sample1.dart diff --git a/example/lib/presentation/resources/app_assets.dart b/example/lib/presentation/resources/app_assets.dart index 3575724f8..000b5e5cf 100644 --- a/example/lib/presentation/resources/app_assets.dart +++ b/example/lib/presentation/resources/app_assets.dart @@ -13,6 +13,8 @@ class AppAssets { return 'assets/icons/ic_scatter_chart.svg'; case ChartType.radar: return 'assets/icons/ic_radar_chart.svg'; + case ChartType.gauge: + return 'assets/icons/ic_radar_chart.svg'; } } diff --git a/example/lib/presentation/samples/chart_sample.dart b/example/lib/presentation/samples/chart_sample.dart index 6d753be25..d15291e4f 100644 --- a/example/lib/presentation/samples/chart_sample.dart +++ b/example/lib/presentation/samples/chart_sample.dart @@ -40,3 +40,9 @@ class RadarChartSample extends ChartSample { @override ChartType get type => ChartType.radar; } + +class GaugeChartSample extends ChartSample { + GaugeChartSample(super.number, super.builder); + @override + ChartType get type => ChartType.gauge; +} \ No newline at end of file diff --git a/example/lib/presentation/samples/chart_samples.dart b/example/lib/presentation/samples/chart_samples.dart index 4114cd5f6..22eaf47ed 100644 --- a/example/lib/presentation/samples/chart_samples.dart +++ b/example/lib/presentation/samples/chart_samples.dart @@ -1,3 +1,4 @@ +import 'package:fl_chart_app/presentation/samples/gauge/guage_chart_sample1.dart'; import 'package:fl_chart_app/util/app_helper.dart'; import 'bar/bar_chart_sample1.dart'; @@ -62,5 +63,8 @@ class ChartSamples { ChartType.radar: [ RadarChartSample(1, (context) => RadarChartSample1()), ], + ChartType.gauge: [ + GaugeChartSample(1, (context) => const GaugeChartSample1()), + ], }; } diff --git a/example/lib/presentation/samples/gauge/guage_chart_sample1.dart b/example/lib/presentation/samples/gauge/guage_chart_sample1.dart new file mode 100644 index 000000000..8925b95b2 --- /dev/null +++ b/example/lib/presentation/samples/gauge/guage_chart_sample1.dart @@ -0,0 +1,52 @@ +import 'package:fl_chart_app/presentation/resources/app_resources.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; + + +class GaugeChartSample1 extends StatefulWidget { + const GaugeChartSample1({super.key}); + + @override + State createState() => GaugeChartSample1State(); +} + +class GaugeChartSample1State extends State { + double _value = 0.7; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(28), + child: Column( + children: [ + SizedBox( + width: 250, + height: 250, + child: GaugeChart( + GaugeChartData( + value: _value, + valueColor: VariableGaugeColor( + colors: [AppColors.contentColorYellow, AppColors.contentColorBlue, AppColors.contentColorRed], + limits: [0.35, 0.5], + ), + backgroundColor: AppColors.contentColorPurple, + strokeWidth: 30, + startAngle: 45, + endAngle: -225, + strokeCap: StrokeCap.round, + ticks: const GaugeTicks( + count: 11, + color: AppColors.contentColorCyan, + radius: 5, + position: GaugeTickPosition.inner, + margin: 5, + ), + ), + ), + ), + Slider(value: _value, onChanged: (v) => setState(() => _value = v)), + ], + ), + ); + } +} \ No newline at end of file diff --git a/example/lib/util/app_helper.dart b/example/lib/util/app_helper.dart index 38c5ac882..4d6f2a7d8 100644 --- a/example/lib/util/app_helper.dart +++ b/example/lib/util/app_helper.dart @@ -1,7 +1,7 @@ import 'package:fl_chart_app/presentation/resources/app_resources.dart'; import 'package:fl_chart_app/urls.dart'; -enum ChartType { line, bar, pie, scatter, radar } +enum ChartType { line, bar, pie, scatter, radar, gauge } extension ChartTypeExtension on ChartType { String get displayName => '$simpleName Chart'; @@ -12,6 +12,7 @@ extension ChartTypeExtension on ChartType { ChartType.pie => 'Pie', ChartType.scatter => 'Scatter', ChartType.radar => 'Radar', + ChartType.gauge => 'Gauge', }; String get documentationUrl => Urls.getChartDocumentationUrl(this); diff --git a/lib/src/chart/gauge_chart/gauge_chart.dart b/lib/src/chart/gauge_chart/gauge_chart.dart index d50b82b82..c73816025 100644 --- a/lib/src/chart/gauge_chart/gauge_chart.dart +++ b/lib/src/chart/gauge_chart/gauge_chart.dart @@ -1,30 +1,30 @@ import 'package:fl_chart/src/chart/gauge_chart/gauge_chart_data.dart'; import 'package:fl_chart/src/chart/gauge_chart/gauge_chart_renderer.dart'; -import 'package:flutter/cupertino.dart'; +import 'package:flutter/widgets.dart'; class GaugeChart extends ImplicitlyAnimatedWidget { - /// Determines how the [GaugeChartData] should be look like. - final GaugeChartData data; - - /// We pass this key to our renderers which are supposed to - /// render the chart itself (without anything around the chart). - final Key? chartRendererKey; - /// [data] determines how the [GaugeChart] should be look like, /// when you make any change in the [GaugeChartData], it updates /// new values with animation, and duration is [swapAnimationDuration]. /// also you can change the [swapAnimationCurve] /// which default is [Curves.linear]. const GaugeChart( - this.data, { - this.chartRendererKey, - Key? key, - Duration swapAnimationDuration = const Duration(milliseconds: 150), - Curve swapAnimationCurve = Curves.linear, - }) : super( - key: key, - duration: swapAnimationDuration, - curve: swapAnimationCurve); + this.data, { + this.chartRendererKey, + super.key, + Duration swapAnimationDuration = const Duration(milliseconds: 150), + Curve swapAnimationCurve = Curves.linear, + }) : super( + duration: swapAnimationDuration, + curve: swapAnimationCurve, + ); + + /// Determines how the [GaugeChartData] should be look like. + final GaugeChartData data; + + /// We pass this key to our renderers which are supposed to + /// render the chart itself (without anything around the chart). + final Key? chartRendererKey; /// Creates a [_GaugeChartState] @override @@ -51,11 +51,12 @@ class _GaugeChartState extends AnimatedWidgetBaseState { } @override - void forEachTween(visitor) { + void forEachTween(TweenVisitor visitor) { _gaugeChartDataTween = visitor( _gaugeChartDataTween, widget.data, - (dynamic value) => GaugeChartDataTween(begin: value, end: widget.data), - ) as GaugeChartDataTween; + (dynamic value) => + GaugeChartDataTween(begin: value as GaugeChartData, end: widget.data), + ) as GaugeChartDataTween?; } -} \ No newline at end of file +} diff --git a/lib/src/chart/gauge_chart/gauge_chart_data.dart b/lib/src/chart/gauge_chart/gauge_chart_data.dart index 792bc59a9..51a7fc6da 100644 --- a/lib/src/chart/gauge_chart/gauge_chart_data.dart +++ b/lib/src/chart/gauge_chart/gauge_chart_data.dart @@ -3,16 +3,15 @@ import 'dart:ui'; import 'package:equatable/equatable.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/src/utils/lerp.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; class ColoredTick { + const ColoredTick(this.position, this.color); final double position; final Color color; - - const ColoredTick(this.position, this.color); } -abstract class ColoredTicksGenerator { +mixin ColoredTicksGenerator { Iterable getColoredTicks(); } @@ -26,9 +25,10 @@ abstract class GaugeColor { } class _LerpGaugeColor implements GaugeColor, ColoredTicksGenerator { - final GaugeColor a, b; - final double t; _LerpGaugeColor(this.a, this.b, this.t); + final GaugeColor a; + final GaugeColor b; + final double t; @override Color getColor(double value) { @@ -52,33 +52,32 @@ class _LerpGaugeColor implements GaugeColor, ColoredTicksGenerator { @immutable class SimpleGaugeColor implements GaugeColor { - final Color color; const SimpleGaugeColor({required this.color}); + final Color color; @override Color getColor(double value) => color; } class VariableGaugeColor implements GaugeColor, ColoredTicksGenerator { - final List limits; - final List colors; - VariableGaugeColor({ required this.limits, required this.colors, - }) : assert( - colors.length - 1 == limits.length, - 'length of limits should be equals to colors length minus one' - ) { - assert( - limits.reduce((a, b) => a < b ? 0 : 2) == 0, - 'the limits list should be sorted in ascending order', - ); - assert( - limits.first > 0 || limits.last < 1.0, - 'limits values should be in range 0, 1 (exclusive)', - ); - } + }) : assert( + colors.length - 1 == limits.length, + 'length of limits should be equals to colors length minus one', + ), + assert( + limits.reduce((a, b) => a < b ? 0 : 2) == 0, + 'the limits list should be sorted in ascending order', + ), + assert( + limits.first > 0 || limits.last < 1.0, + 'limits values should be in range 0, 1 (exclusive)', + ); + + final List limits; + final List colors; @override Color getColor(double value) { @@ -90,7 +89,7 @@ class VariableGaugeColor implements GaugeColor, ColoredTicksGenerator { @override Iterable getColoredTicks() sync* { - for(var i = 0; i < limits.length; i++) { + for (var i = 0; i < limits.length; i++) { yield ColoredTick(limits[i], colors[i + 1]); } } @@ -104,13 +103,6 @@ enum GaugeTickPosition { @immutable class GaugeTicks { - final int count; - final double radius; - final Color color; - final GaugeTickPosition position; - final double margin; - final bool showChangingColorTicks; - const GaugeTicks({ this.count = 3, this.radius = 3.0, @@ -118,8 +110,14 @@ class GaugeTicks { this.position = GaugeTickPosition.outer, this.margin = 3, this.showChangingColorTicks = true, - }) : assert(count > 2, 'count should be >= 2'), + }) : assert(count > 2, 'count should be >= 2'), assert(radius > 0, 'radius should be > 0'); + final int count; + final double radius; + final Color color; + final GaugeTickPosition position; + final double margin; + final bool showChangingColorTicks; static GaugeTicks? lerp(GaugeTicks? a, GaugeTicks? b, double t) { if (a == null || b == null) return b; @@ -135,16 +133,6 @@ class GaugeTicks { } class GaugeChartData extends BaseChartData with EquatableMixin { - final StrokeCap strokeCap; - final double value; - final double strokeWidth; - final GaugeColor valueColor; - final Color? backgroundColor; - final double startAngle; - final double endAngle; - final GaugeTouchData gaugeTouchData; - final GaugeTicks? ticks; - GaugeChartData({ this.strokeCap = StrokeCap.butt, this.backgroundColor, @@ -154,12 +142,19 @@ class GaugeChartData extends BaseChartData with EquatableMixin { required this.startAngle, required this.endAngle, this.ticks, - FlBorderData? borderData, + super.borderData, GaugeTouchData? touchData, - }) : gaugeTouchData = touchData ?? GaugeTouchData(), - super( - borderData: borderData, - touchData: touchData ?? GaugeTouchData()); + }) : gaugeTouchData = touchData ?? GaugeTouchData(), + super(touchData: touchData ?? GaugeTouchData()); + final StrokeCap strokeCap; + final double value; + final double strokeWidth; + final GaugeColor valueColor; + final Color? backgroundColor; + final double startAngle; + final double endAngle; + final GaugeTouchData gaugeTouchData; + final GaugeTicks? ticks; GaugeChartData copyWith({ StrokeCap? strokeCap, @@ -186,7 +181,6 @@ class GaugeChartData extends BaseChartData with EquatableMixin { touchData: gaugeTouchData ?? this.gaugeTouchData, ); - @override GaugeChartData lerp(BaseChartData a, BaseChartData b, double t) { if (a is GaugeChartData && b is GaugeChartData) { @@ -207,23 +201,24 @@ class GaugeChartData extends BaseChartData with EquatableMixin { @override List get props => [ - ticks, - strokeCap, - backgroundColor, - valueColor, - value, - strokeWidth, - startAngle, - endAngle, - ]; + ticks, + strokeCap, + backgroundColor, + valueColor, + value, + strokeWidth, + startAngle, + endAngle, + ]; } class GaugeTouchData extends FlTouchData { GaugeTouchData({ bool? enabled, BaseTouchCallback? touchCallback, - MouseCursorResolver? mouseCursorResolver - }) : super(enabled ?? true, touchCallback, mouseCursorResolver); + MouseCursorResolver? mouseCursorResolver, + Duration? longPressDuration, + }) : super(enabled ?? true, touchCallback, mouseCursorResolver, longPressDuration); } class GaugeTouchResponse extends BaseTouchResponse { @@ -244,4 +239,4 @@ class GaugeChartDataTween extends Tween { /// Lerps a [GaugeChartData] based on [t] value, check [Tween.lerp]. @override GaugeChartData lerp(double t) => begin!.lerp(begin!, end!, t); -} \ No newline at end of file +} diff --git a/lib/src/chart/gauge_chart/gauge_chart_painter.dart b/lib/src/chart/gauge_chart/gauge_chart_painter.dart index 7a9cdb27b..119d5355c 100644 --- a/lib/src/chart/gauge_chart/gauge_chart_painter.dart +++ b/lib/src/chart/gauge_chart/gauge_chart_painter.dart @@ -7,7 +7,6 @@ import 'package:fl_chart/src/utils/utils.dart'; import 'package:flutter/material.dart'; class GaugeChartPainter extends BaseChartPainter { - late Paint _backgroundPaint, _valuePaint, _tickPaint; GaugeChartPainter() : super() { _backgroundPaint = Paint() @@ -17,6 +16,10 @@ class GaugeChartPainter extends BaseChartPainter { _tickPaint = Paint(); } + late Paint _backgroundPaint; + late Paint _valuePaint; + late Paint _tickPaint; + @override void paint(BuildContext context, CanvasWrapper canvasWrapper, PaintHolder holder) { @@ -59,19 +62,12 @@ class GaugeChartPainter extends BaseChartPainter { } } - _drawTick(CanvasWrapper canvasWrapper, Offset center, double angle, double radius, GaugeTicks ticks, double strokeWidth) { - double positionRadius; - switch (ticks.position) { - case GaugeTickPosition.inner: - positionRadius = radius - strokeWidth - ticks.radius - ticks.margin; - break; - case GaugeTickPosition.outer: - positionRadius = radius + ticks.radius + ticks.margin; - break; - case GaugeTickPosition.center: - positionRadius = radius - strokeWidth / 2; - break; - } + void _drawTick(CanvasWrapper canvasWrapper, Offset center, double angle, double radius, GaugeTicks ticks, double strokeWidth) { + final positionRadius = switch (ticks.position) { + GaugeTickPosition.inner => radius - strokeWidth - ticks.radius - ticks.margin, + GaugeTickPosition.outer => radius + ticks.radius + ticks.margin, + GaugeTickPosition.center => radius - strokeWidth / 2, + }; final tickX = center.dx + cos(angle) * positionRadius; final tickY = center.dy + sin(angle) * positionRadius; @@ -82,11 +78,11 @@ class GaugeChartPainter extends BaseChartPainter { void drawValue(CanvasWrapper canvasWrapper, PaintHolder holder) { final data = holder.data; - var size = Size.square( + final size = Size.square( canvasWrapper.size.shortestSide - data.strokeWidth, ); final demiStroke = data.strokeWidth / 2; - var offset = Offset( + final offset = Offset( max(canvasWrapper.size.width - canvasWrapper.size.height, 0) / 2 + demiStroke, max(canvasWrapper.size.height - canvasWrapper.size.width, 0) / 2 + demiStroke, ); @@ -140,4 +136,4 @@ class GaugeChartPainter extends BaseChartPainter { @visibleForTesting double gaugeRadius(Size size) => size.shortestSide / 2; -} \ No newline at end of file +} diff --git a/lib/src/chart/gauge_chart/gauge_chart_renderer.dart b/lib/src/chart/gauge_chart/gauge_chart_renderer.dart index 0f389718c..7871bfe19 100644 --- a/lib/src/chart/gauge_chart/gauge_chart_renderer.dart +++ b/lib/src/chart/gauge_chart/gauge_chart_renderer.dart @@ -1,21 +1,28 @@ +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; import 'package:fl_chart/src/chart/base/base_chart/render_base_chart.dart'; +import 'package:fl_chart/src/chart/gauge_chart/gauge_chart_data.dart'; import 'package:fl_chart/src/chart/gauge_chart/gauge_chart_painter.dart'; import 'package:fl_chart/src/utils/canvas_wrapper.dart'; import 'package:flutter/widgets.dart'; -import '../base/base_chart/base_chart_painter.dart'; -import 'gauge_chart_data.dart'; - /// Low level GaugeChart Widget. class GaugeChartLeaf extends LeafRenderObjectWidget { - const GaugeChartLeaf({Key? key, required this.data, required this.targetData}) - : super(key: key); + const GaugeChartLeaf({ + super.key, + required this.data, + required this.targetData, + }); - final GaugeChartData data, targetData; + final GaugeChartData data; + final GaugeChartData targetData; @override RenderGaugeChart createRenderObject(BuildContext context) => RenderGaugeChart( - context, data, targetData, MediaQuery.of(context).textScaleFactor); + context, + data, + targetData, + MediaQuery.of(context).textScaleFactor, + ); @override void updateRenderObject(BuildContext context, RenderGaugeChart renderObject) { @@ -29,9 +36,12 @@ class GaugeChartLeaf extends LeafRenderObjectWidget { /// Renders our RadarChart, also handles hitTest. class RenderGaugeChart extends RenderBaseChart { - RenderGaugeChart(BuildContext context, GaugeChartData data, - GaugeChartData targetData, double textScale) - : _data = data, + RenderGaugeChart( + BuildContext context, + GaugeChartData data, + GaugeChartData targetData, + double textScale, + ) : _data = data, _targetData = targetData, _textScale = textScale, super(targetData.gaugeTouchData, context); @@ -69,7 +79,7 @@ class RenderGaugeChart extends RenderBaseChart { Size? mockTestSize; @visibleForTesting - var painter = GaugeChartPainter(); + GaugeChartPainter painter = GaugeChartPainter(); PaintHolder get paintHolder { return PaintHolder(data, targetData, textScale); @@ -77,9 +87,9 @@ class RenderGaugeChart extends RenderBaseChart { @override void paint(PaintingContext context, Offset offset) { - final canvas = context.canvas; - canvas.save(); - canvas.translate(offset.dx, offset.dy); + final canvas = context.canvas + ..save() + ..translate(offset.dx, offset.dy); painter.paint( buildContext, CanvasWrapper(canvas, mockTestSize ?? size), @@ -90,11 +100,12 @@ class RenderGaugeChart extends RenderBaseChart { @override GaugeTouchResponse getResponseAtLocation(Offset localPosition) { - var touchedSpot = painter.handleTouch( - localPosition, - mockTestSize ?? size, - paintHolder, + return GaugeTouchResponse( + painter.handleTouch( + localPosition, + mockTestSize ?? size, + paintHolder, + ), ); - return GaugeTouchResponse(touchedSpot); } -} \ No newline at end of file +} From e31db9ad5d04ef12b2c9ab97c40049dabcfbc315 Mon Sep 17 00:00:00 2001 From: Florian ARNOULD Date: Fri, 28 Jul 2023 12:53:37 +0200 Subject: [PATCH 3/8] feat: implemented the gauge touch support --- .../presentation/samples/chart_samples.dart | 2 +- ..._sample1.dart => gauge_chart_sample1.dart} | 9 +- .../chart/gauge_chart/gauge_chart_data.dart | 15 +- .../gauge_chart/gauge_chart_painter.dart | 234 ++++++++++++++---- .../gauge_chart/gauge_chart_renderer.dart | 11 +- 5 files changed, 210 insertions(+), 61 deletions(-) rename example/lib/presentation/samples/gauge/{guage_chart_sample1.dart => gauge_chart_sample1.dart} (80%) diff --git a/example/lib/presentation/samples/chart_samples.dart b/example/lib/presentation/samples/chart_samples.dart index 22eaf47ed..fb54df010 100644 --- a/example/lib/presentation/samples/chart_samples.dart +++ b/example/lib/presentation/samples/chart_samples.dart @@ -1,4 +1,4 @@ -import 'package:fl_chart_app/presentation/samples/gauge/guage_chart_sample1.dart'; +import 'package:fl_chart_app/presentation/samples/gauge/gauge_chart_sample1.dart'; import 'package:fl_chart_app/util/app_helper.dart'; import 'bar/bar_chart_sample1.dart'; diff --git a/example/lib/presentation/samples/gauge/guage_chart_sample1.dart b/example/lib/presentation/samples/gauge/gauge_chart_sample1.dart similarity index 80% rename from example/lib/presentation/samples/gauge/guage_chart_sample1.dart rename to example/lib/presentation/samples/gauge/gauge_chart_sample1.dart index 8925b95b2..652f71e59 100644 --- a/example/lib/presentation/samples/gauge/guage_chart_sample1.dart +++ b/example/lib/presentation/samples/gauge/gauge_chart_sample1.dart @@ -12,6 +12,7 @@ class GaugeChartSample1 extends StatefulWidget { class GaugeChartSample1State extends State { double _value = 0.7; + bool _isSelected = false; @override Widget build(BuildContext context) { @@ -29,7 +30,7 @@ class GaugeChartSample1State extends State { colors: [AppColors.contentColorYellow, AppColors.contentColorBlue, AppColors.contentColorRed], limits: [0.35, 0.5], ), - backgroundColor: AppColors.contentColorPurple, + backgroundColor: AppColors.contentColorPurple.withOpacity(_isSelected ? 0.2 : 1), strokeWidth: 30, startAngle: 45, endAngle: -225, @@ -41,6 +42,12 @@ class GaugeChartSample1State extends State { position: GaugeTickPosition.inner, margin: 5, ), + touchData: GaugeTouchData( + enabled: true, + touchCallback: (_, value) => setState(() { + _isSelected = value?.spot != null; + }), + ), ), ), ), diff --git a/lib/src/chart/gauge_chart/gauge_chart_data.dart b/lib/src/chart/gauge_chart/gauge_chart_data.dart index 51a7fc6da..b546811ed 100644 --- a/lib/src/chart/gauge_chart/gauge_chart_data.dart +++ b/lib/src/chart/gauge_chart/gauge_chart_data.dart @@ -222,11 +222,22 @@ class GaugeTouchData extends FlTouchData { } class GaugeTouchResponse extends BaseTouchResponse { - GaugeTouchResponse(GaugeTouchedSpot? touchedSpot); + GaugeTouchResponse(this.spot); + GaugeTouchedSpot? spot; + + @override + String toString() { + return 'GaugeTouchResponse(spot: $spot)'; + } } class GaugeTouchedSpot extends TouchedSpot with EquatableMixin { - GaugeTouchedSpot(FlSpot spot, Offset offset) : super(spot, offset); + GaugeTouchedSpot(super.spot, super.offset); + + @override + String toString() { + return 'GaugeTouchedSpot(spot: $spot, offset: $offset)'; + } } /// It lerps a [GaugeChartData] to another [GaugeChartData] (handles animation for updating values) diff --git a/lib/src/chart/gauge_chart/gauge_chart_painter.dart b/lib/src/chart/gauge_chart/gauge_chart_painter.dart index 119d5355c..584b13922 100644 --- a/lib/src/chart/gauge_chart/gauge_chart_painter.dart +++ b/lib/src/chart/gauge_chart/gauge_chart_painter.dart @@ -1,18 +1,15 @@ import 'dart:math'; +import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; -import 'package:fl_chart/src/chart/gauge_chart/gauge_chart_data.dart'; import 'package:fl_chart/src/utils/canvas_wrapper.dart'; import 'package:fl_chart/src/utils/utils.dart'; import 'package:flutter/material.dart'; class GaugeChartPainter extends BaseChartPainter { - GaugeChartPainter() : super() { - _backgroundPaint = Paint() - ..isAntiAlias = true; - _valuePaint = Paint() - ..isAntiAlias = true; + _backgroundPaint = Paint()..isAntiAlias = true; + _valuePaint = Paint()..isAntiAlias = true; _tickPaint = Paint(); } @@ -20,18 +17,25 @@ class GaugeChartPainter extends BaseChartPainter { late Paint _valuePaint; late Paint _tickPaint; + GaugePosition? _gaugePosition; + @override - void paint(BuildContext context, CanvasWrapper canvasWrapper, - PaintHolder holder) { + void paint( + BuildContext context, + CanvasWrapper canvasWrapper, + PaintHolder holder, + ) { super.paint(context, canvasWrapper, holder); drawValue(canvasWrapper, holder); drawTicks(canvasWrapper, holder); } - + @visibleForTesting - void drawTicks(CanvasWrapper canvasWrapper, - PaintHolder holder) { + void drawTicks( + CanvasWrapper canvasWrapper, + PaintHolder holder, + ) { final data = holder.data; final ticks = data.ticks; if (ticks == null) return; @@ -47,26 +51,50 @@ class GaugeChartPainter extends BaseChartPainter { final radius = gaugeRadius(size); /// draw radar ticks - for(var i = 0; i < ticks.count; i++) { + for (var i = 0; i < ticks.count; i++) { final angle = Utils().radians(data.startAngle + interTickAngle * i); - _drawTick(canvasWrapper, centerOffset, angle, radius, ticks, data.strokeWidth); + _drawTick( + canvasWrapper, + centerOffset, + angle, + radius, + ticks, + data.strokeWidth, + ); } final valueColor = data.valueColor; if (ticks.showChangingColorTicks && valueColor is ColoredTicksGenerator) { - for(final tick in (valueColor as ColoredTicksGenerator).getColoredTicks()) { - final angle = Utils().radians(data.startAngle + angleRange * tick.position); + for (final tick + in (valueColor as ColoredTicksGenerator).getColoredTicks()) { + final angle = + Utils().radians(data.startAngle + angleRange * tick.position); _tickPaint.color = tick.color; - _drawTick(canvasWrapper, centerOffset, angle, radius, ticks, data.strokeWidth); + _drawTick( + canvasWrapper, + centerOffset, + angle, + radius, + ticks, + data.strokeWidth, + ); } } } - void _drawTick(CanvasWrapper canvasWrapper, Offset center, double angle, double radius, GaugeTicks ticks, double strokeWidth) { + void _drawTick( + CanvasWrapper canvasWrapper, + Offset center, + double angle, + double radius, + GaugeTicks ticks, + double strokeWidth, + ) { final positionRadius = switch (ticks.position) { - GaugeTickPosition.inner => radius - strokeWidth - ticks.radius - ticks.margin, + GaugeTickPosition.inner => + radius - strokeWidth - ticks.radius - ticks.margin, GaugeTickPosition.outer => radius + ticks.radius + ticks.margin, - GaugeTickPosition.center => radius - strokeWidth / 2, + GaugeTickPosition.center => radius - strokeWidth / 2, }; final tickX = center.dx + cos(angle) * positionRadius; final tickY = center.dy + sin(angle) * positionRadius; @@ -74,52 +102,71 @@ class GaugeChartPainter extends BaseChartPainter { canvasWrapper.drawCircle(Offset(tickX, tickY), ticks.radius, _tickPaint); } - @visibleForTesting - void drawValue(CanvasWrapper canvasWrapper, - PaintHolder holder) { + GaugePosition _calculateValuePosition( + Size viewSize, + PaintHolder holder, + ) { final data = holder.data; final size = Size.square( - canvasWrapper.size.shortestSide - data.strokeWidth, + viewSize.shortestSide - data.strokeWidth, ); final demiStroke = data.strokeWidth / 2; final offset = Offset( - max(canvasWrapper.size.width - canvasWrapper.size.height, 0) / 2 + demiStroke, - max(canvasWrapper.size.height - canvasWrapper.size.width, 0) / 2 + demiStroke, + max(viewSize.width - viewSize.height, 0) / 2 + demiStroke, + max(viewSize.height - viewSize.width, 0) / 2 + demiStroke, ); - final backgroundColor = data.backgroundColor; - final angleRange = data.endAngle - data.startAngle; + return GaugePosition( + offset & size, + data.strokeCap, + data.strokeWidth, + angleRange, + data.startAngle, + angleRange * data.value.clamp(0, 1), + ); + } - // for(var i = 0; i < 3; i++) { - /// Draw background if needed - if(backgroundColor != null) { - _backgroundPaint - ..color = backgroundColor - ..strokeWidth = data.strokeWidth - ..strokeCap = data.strokeCap - ..style = PaintingStyle.stroke; - canvasWrapper.drawArc( - offset & size, - Utils().radians(data.startAngle), - Utils().radians(angleRange), - false, - _backgroundPaint, - ); - } + @visibleForTesting + void drawValue( + CanvasWrapper canvasWrapper, + PaintHolder holder, + ) { + final data = holder.data; + + final backgroundColor = data.backgroundColor; + final position = + _gaugePosition = _calculateValuePosition(canvasWrapper.size, holder); - /// Draw value - _valuePaint - ..color = data.valueColor.getColor(data.value) + // for(var i = 0; i < 3; i++) { + /// Draw background if needed + if (backgroundColor != null) { + _backgroundPaint + ..color = backgroundColor ..strokeWidth = data.strokeWidth ..strokeCap = data.strokeCap ..style = PaintingStyle.stroke; canvasWrapper.drawArc( - offset & size, - Utils().radians(data.startAngle), - Utils().radians(angleRange * data.value.clamp(0, 1)), + position.rect, + Utils().radians(position.startAngle), + Utils().radians(position.angleRange), false, - _valuePaint, + _backgroundPaint, ); + } + + /// Draw value + _valuePaint + ..color = data.valueColor.getColor(data.value) + ..strokeWidth = data.strokeWidth + ..strokeCap = data.strokeCap + ..style = PaintingStyle.stroke; + canvasWrapper.drawArc( + position.rect, + Utils().radians(position.startAngle), + Utils().radians(position.angleSize), + false, + _valuePaint, + ); // offset = offset + Offset(data.strokeWidth + 3, data.strokeWidth + 3); // size = Size.square(size.width - 2 * (data.strokeWidth + 3)); @@ -127,7 +174,19 @@ class GaugeChartPainter extends BaseChartPainter { } GaugeTouchedSpot? handleTouch( - Offset touchedPoint, Size viewSize, PaintHolder holder) { + Offset touchedPoint, + Size viewSize, + PaintHolder holder, + ) { + final position = + _gaugePosition ??= _calculateValuePosition(viewSize, holder); + if (position.contains(touchedPoint)) { + final offset = position.getInterestSpot(); + return GaugeTouchedSpot( + FlSpot(offset.dx, offset.dy), + offset, + ); + } return null; } @@ -137,3 +196,76 @@ class GaugeChartPainter extends BaseChartPainter { @visibleForTesting double gaugeRadius(Size size) => size.shortestSide / 2; } + +class GaugePosition { + GaugePosition( + this.rect, + this.strokeCap, + this.strokeWidth, + this.angleRange, + this.startAngle, + this.angleSize, + ); + + final Rect rect; + final double strokeWidth; + final double angleRange; + final double startAngle; + final double angleSize; + final StrokeCap strokeCap; + + Range _calculateValidValueRange() { + final radius = rect.shortestSide / 2; + final halfStroke = strokeWidth / 2; + return Range.fromValues(radius - halfStroke, radius + halfStroke); + } + + Offset getInterestSpot() { + final radius = rect.shortestSide / 2; + final averageAngle = startAngle + angleSize / 2; + return rect.center + + Offset.fromDirection(Utils().radians(averageAngle), radius); + } + + bool contains(Offset point) { + final vector = point - rect.center; + var start = startAngle; + var end = angleSize < 0 ? angleSize + startAngle : angleSize - startAngle; + if (strokeCap != StrokeCap.butt) { + final strokeRadius = strokeWidth / 2; + final radius = rect.shortestSide / 2; + final bonusAngle = 180 * strokeRadius / (pi * radius); + start = angleSize < 0 ? start + bonusAngle : start - bonusAngle; + end = angleSize < 0 ? end - bonusAngle : end + bonusAngle; + } + return _calculateValidValueRange().contains(vector.distance) && + DegreeAngleRange(start, end) + .contains(Utils().degrees(vector.direction)); + } +} + +class Range { + const Range._(this.min, this.max); + factory Range.fromValues(double a, double b) { + return a > b ? Range._(b, a) : Range._(a, b); + } + + final double min; + final double max; + + bool contains(double value) { + return min <= value && max >= value; + } +} + +class DegreeAngleRange { + const DegreeAngleRange(this.start, this.end); + final double start; + final double end; + + bool contains(double angle) { + final ref = (end - start) < 0 ? end - start + 360 : end - start; + final value = (angle - start) < 0 ? angle - start + 360 : angle - start; + return end < start ? value > ref : value < ref; + } +} diff --git a/lib/src/chart/gauge_chart/gauge_chart_renderer.dart b/lib/src/chart/gauge_chart/gauge_chart_renderer.dart index 7871bfe19..346a18a3f 100644 --- a/lib/src/chart/gauge_chart/gauge_chart_renderer.dart +++ b/lib/src/chart/gauge_chart/gauge_chart_renderer.dart @@ -100,12 +100,11 @@ class RenderGaugeChart extends RenderBaseChart { @override GaugeTouchResponse getResponseAtLocation(Offset localPosition) { - return GaugeTouchResponse( - painter.handleTouch( - localPosition, - mockTestSize ?? size, - paintHolder, - ), + final touchedSpot = painter.handleTouch( + localPosition, + mockTestSize ?? size, + paintHolder, ); + return GaugeTouchResponse(touchedSpot); } } From 5fc9b8b461a0ebf96d05a2f15b4f3c869efc52da Mon Sep 17 00:00:00 2001 From: Florian ARNOULD Date: Thu, 3 Aug 2023 14:37:38 +0200 Subject: [PATCH 4/8] chore: reformat --- example/lib/presentation/samples/chart_sample.dart | 2 +- .../samples/gauge/gauge_chart_sample1.dart | 12 ++++++++---- lib/fl_chart.dart | 4 ++-- lib/src/chart/gauge_chart/gauge_chart_data.dart | 7 ++++++- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/example/lib/presentation/samples/chart_sample.dart b/example/lib/presentation/samples/chart_sample.dart index d15291e4f..83ad72c41 100644 --- a/example/lib/presentation/samples/chart_sample.dart +++ b/example/lib/presentation/samples/chart_sample.dart @@ -45,4 +45,4 @@ class GaugeChartSample extends ChartSample { GaugeChartSample(super.number, super.builder); @override ChartType get type => ChartType.gauge; -} \ No newline at end of file +} diff --git a/example/lib/presentation/samples/gauge/gauge_chart_sample1.dart b/example/lib/presentation/samples/gauge/gauge_chart_sample1.dart index 652f71e59..219209233 100644 --- a/example/lib/presentation/samples/gauge/gauge_chart_sample1.dart +++ b/example/lib/presentation/samples/gauge/gauge_chart_sample1.dart @@ -2,7 +2,6 @@ import 'package:fl_chart_app/presentation/resources/app_resources.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; - class GaugeChartSample1 extends StatefulWidget { const GaugeChartSample1({super.key}); @@ -27,10 +26,15 @@ class GaugeChartSample1State extends State { GaugeChartData( value: _value, valueColor: VariableGaugeColor( - colors: [AppColors.contentColorYellow, AppColors.contentColorBlue, AppColors.contentColorRed], + colors: [ + AppColors.contentColorYellow, + AppColors.contentColorBlue, + AppColors.contentColorRed + ], limits: [0.35, 0.5], ), - backgroundColor: AppColors.contentColorPurple.withOpacity(_isSelected ? 0.2 : 1), + backgroundColor: AppColors.contentColorPurple + .withOpacity(_isSelected ? 0.2 : 1), strokeWidth: 30, startAngle: 45, endAngle: -225, @@ -56,4 +60,4 @@ class GaugeChartSample1State extends State { ), ); } -} \ No newline at end of file +} diff --git a/lib/fl_chart.dart b/lib/fl_chart.dart index 233a92955..95cea2b67 100644 --- a/lib/fl_chart.dart +++ b/lib/fl_chart.dart @@ -7,6 +7,8 @@ export 'src/chart/base/axis_chart/axis_chart_data.dart'; export 'src/chart/base/axis_chart/axis_chart_widgets.dart'; export 'src/chart/base/base_chart/base_chart_data.dart'; export 'src/chart/base/base_chart/fl_touch_event.dart'; +export 'src/chart/gauge_chart/gauge_chart.dart'; +export 'src/chart/gauge_chart/gauge_chart_data.dart'; export 'src/chart/line_chart/line_chart.dart'; export 'src/chart/line_chart/line_chart_data.dart'; export 'src/chart/pie_chart/pie_chart.dart'; @@ -15,5 +17,3 @@ 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/chart/gauge_chart/gauge_chart.dart'; -export 'src/chart/gauge_chart/gauge_chart_data.dart'; diff --git a/lib/src/chart/gauge_chart/gauge_chart_data.dart b/lib/src/chart/gauge_chart/gauge_chart_data.dart index b546811ed..5873d107d 100644 --- a/lib/src/chart/gauge_chart/gauge_chart_data.dart +++ b/lib/src/chart/gauge_chart/gauge_chart_data.dart @@ -218,7 +218,12 @@ class GaugeTouchData extends FlTouchData { BaseTouchCallback? touchCallback, MouseCursorResolver? mouseCursorResolver, Duration? longPressDuration, - }) : super(enabled ?? true, touchCallback, mouseCursorResolver, longPressDuration); + }) : super( + enabled ?? true, + touchCallback, + mouseCursorResolver, + longPressDuration, + ); } class GaugeTouchResponse extends BaseTouchResponse { From 772475549853fa5c0e086402f0a92736de12e358 Mon Sep 17 00:00:00 2001 From: Florian ARNOULD Date: Sat, 5 Aug 2023 15:04:36 +0200 Subject: [PATCH 5/8] chore: added gauge tests --- .../chart/gauge_chart/gauge_chart_data.dart | 59 +- .../gauge_chart/gauge_chart_painter.dart | 7 +- .../gauge_chart/gauge_chart_renderer.dart | 5 +- test/chart/data_pool.dart | 60 + .../gauge_chart/gauge_chart_data_test.dart | 359 +++++ .../gauge_chart/gauge_chart_painter_test.dart | 420 ++++++ .../gauge_chart_painter_test.mocks.dart | 1339 +++++++++++++++++ .../gauge_chart_renderer_test.dart | 134 ++ .../gauge_chart_renderer_test.mocks.dart | 1270 ++++++++++++++++ test/utils/utils_test.mocks.dart | 2 +- 10 files changed, 3622 insertions(+), 33 deletions(-) create mode 100644 test/chart/gauge_chart/gauge_chart_data_test.dart create mode 100644 test/chart/gauge_chart/gauge_chart_painter_test.dart create mode 100644 test/chart/gauge_chart/gauge_chart_painter_test.mocks.dart create mode 100644 test/chart/gauge_chart/gauge_chart_renderer_test.dart create mode 100644 test/chart/gauge_chart/gauge_chart_renderer_test.mocks.dart diff --git a/lib/src/chart/gauge_chart/gauge_chart_data.dart b/lib/src/chart/gauge_chart/gauge_chart_data.dart index 5873d107d..09a566701 100644 --- a/lib/src/chart/gauge_chart/gauge_chart_data.dart +++ b/lib/src/chart/gauge_chart/gauge_chart_data.dart @@ -5,10 +5,13 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/src/utils/lerp.dart'; import 'package:flutter/widgets.dart'; -class ColoredTick { +class ColoredTick with EquatableMixin { const ColoredTick(this.position, this.color); final double position; final Color color; + + @override + List get props => [position, color]; } mixin ColoredTicksGenerator { @@ -51,15 +54,18 @@ class _LerpGaugeColor implements GaugeColor, ColoredTicksGenerator { } @immutable -class SimpleGaugeColor implements GaugeColor { +class SimpleGaugeColor with EquatableMixin implements GaugeColor { const SimpleGaugeColor({required this.color}); final Color color; @override Color getColor(double value) => color; + + @override + List get props => [color]; } -class VariableGaugeColor implements GaugeColor, ColoredTicksGenerator { +class VariableGaugeColor with EquatableMixin implements GaugeColor, ColoredTicksGenerator { VariableGaugeColor({ required this.limits, required this.colors, @@ -68,7 +74,7 @@ class VariableGaugeColor implements GaugeColor, ColoredTicksGenerator { 'length of limits should be equals to colors length minus one', ), assert( - limits.reduce((a, b) => a < b ? 0 : 2) == 0, + limits.length <= 1 || limits.reduce((a, b) => a < b ? 0 : 2) == 0, 'the limits list should be sorted in ascending order', ), assert( @@ -93,6 +99,9 @@ class VariableGaugeColor implements GaugeColor, ColoredTicksGenerator { yield ColoredTick(limits[i], colors[i + 1]); } } + + @override + List get props => [limits, colors]; } enum GaugeTickPosition { @@ -102,7 +111,7 @@ enum GaugeTickPosition { } @immutable -class GaugeTicks { +class GaugeTicks with EquatableMixin { const GaugeTicks({ this.count = 3, this.radius = 3.0, @@ -120,6 +129,9 @@ class GaugeTicks { final bool showChangingColorTicks; static GaugeTicks? lerp(GaugeTicks? a, GaugeTicks? b, double t) { + // TODO(FlorianArnould): if showChangingColorTicks are different + // or just a or b is null, handle this with a fade like effect by replacing + // the null value with a default one if (a == null || b == null) return b; return GaugeTicks( color: Color.lerp(a.color, b.color, t)!, @@ -130,6 +142,9 @@ class GaugeTicks { showChangingColorTicks: b.showChangingColorTicks, ); } + + @override + List get props => [count, radius, color, position, margin, showChangingColorTicks]; } class GaugeChartData extends BaseChartData with EquatableMixin { @@ -142,7 +157,6 @@ class GaugeChartData extends BaseChartData with EquatableMixin { required this.startAngle, required this.endAngle, this.ticks, - super.borderData, GaugeTouchData? touchData, }) : gaugeTouchData = touchData ?? GaugeTouchData(), super(touchData: touchData ?? GaugeTouchData()); @@ -177,7 +191,6 @@ class GaugeChartData extends BaseChartData with EquatableMixin { startAngle: startAngle ?? this.startAngle, endAngle: endAngle ?? this.endAngle, ticks: ticks ?? this.ticks, - borderData: borderData ?? this.borderData, touchData: gaugeTouchData ?? this.gaugeTouchData, ); @@ -193,6 +206,7 @@ class GaugeChartData extends BaseChartData with EquatableMixin { strokeWidth: lerpDouble(a.strokeWidth, b.strokeWidth, t)!, startAngle: lerpDouble(a.startAngle, b.startAngle, t)!, endAngle: lerpDouble(a.endAngle, b.endAngle, t)!, + touchData: b.gaugeTouchData, ); } else { throw Exception('Illegal State'); @@ -201,15 +215,18 @@ class GaugeChartData extends BaseChartData with EquatableMixin { @override List get props => [ - ticks, - strokeCap, - backgroundColor, - valueColor, - value, - strokeWidth, - startAngle, - endAngle, - ]; + ticks, + strokeCap, + backgroundColor, + valueColor, + value, + strokeWidth, + startAngle, + endAngle, + gaugeTouchData, + ticks, + borderData + ]; } class GaugeTouchData extends FlTouchData { @@ -229,20 +246,10 @@ class GaugeTouchData extends FlTouchData { class GaugeTouchResponse extends BaseTouchResponse { GaugeTouchResponse(this.spot); GaugeTouchedSpot? spot; - - @override - String toString() { - return 'GaugeTouchResponse(spot: $spot)'; - } } class GaugeTouchedSpot extends TouchedSpot with EquatableMixin { GaugeTouchedSpot(super.spot, super.offset); - - @override - String toString() { - return 'GaugeTouchedSpot(spot: $spot, offset: $offset)'; - } } /// It lerps a [GaugeChartData] to another [GaugeChartData] (handles animation for updating values) diff --git a/lib/src/chart/gauge_chart/gauge_chart_painter.dart b/lib/src/chart/gauge_chart/gauge_chart_painter.dart index 584b13922..9afea557a 100644 --- a/lib/src/chart/gauge_chart/gauge_chart_painter.dart +++ b/lib/src/chart/gauge_chart/gauge_chart_painter.dart @@ -50,7 +50,7 @@ class GaugeChartPainter extends BaseChartPainter { final radius = gaugeRadius(size); - /// draw radar ticks + /// draw gauge ticks for (var i = 0; i < ticks.count; i++) { final angle = Utils().radians(data.startAngle + interTickAngle * i); _drawTick( @@ -63,6 +63,7 @@ class GaugeChartPainter extends BaseChartPainter { ); } + // draw changing color ticks final valueColor = data.valueColor; if (ticks.showChangingColorTicks && valueColor is ColoredTicksGenerator) { for (final tick @@ -137,7 +138,6 @@ class GaugeChartPainter extends BaseChartPainter { final position = _gaugePosition = _calculateValuePosition(canvasWrapper.size, holder); - // for(var i = 0; i < 3; i++) { /// Draw background if needed if (backgroundColor != null) { _backgroundPaint @@ -168,9 +168,6 @@ class GaugeChartPainter extends BaseChartPainter { _valuePaint, ); - // offset = offset + Offset(data.strokeWidth + 3, data.strokeWidth + 3); - // size = Size.square(size.width - 2 * (data.strokeWidth + 3)); - // } } GaugeTouchedSpot? handleTouch( diff --git a/lib/src/chart/gauge_chart/gauge_chart_renderer.dart b/lib/src/chart/gauge_chart/gauge_chart_renderer.dart index 346a18a3f..9a6147b35 100644 --- a/lib/src/chart/gauge_chart/gauge_chart_renderer.dart +++ b/lib/src/chart/gauge_chart/gauge_chart_renderer.dart @@ -5,6 +5,8 @@ import 'package:fl_chart/src/chart/gauge_chart/gauge_chart_painter.dart'; import 'package:fl_chart/src/utils/canvas_wrapper.dart'; import 'package:flutter/widgets.dart'; +// coverage:ignore-start + /// Low level GaugeChart Widget. class GaugeChartLeaf extends LeafRenderObjectWidget { const GaugeChartLeaf({ @@ -33,8 +35,9 @@ class GaugeChartLeaf extends LeafRenderObjectWidget { ..buildContext = context; } } +// coverage:ignore-end -/// Renders our RadarChart, also handles hitTest. +/// Renders our GaugeChart, also handles hitTest. class RenderGaugeChart extends RenderBaseChart { RenderGaugeChart( BuildContext context, diff --git a/test/chart/data_pool.dart b/test/chart/data_pool.dart index 2bb1f1315..a643fb793 100644 --- a/test/chart/data_pool.dart +++ b/test/chart/data_pool.dart @@ -3174,3 +3174,63 @@ final DefaultTextStyle defaultTextStyle1 = DefaultTextStyle( style: const TextStyle(), child: Container(), ); + +final GaugeTouchData gaugeTouchData1Clone = gaugeTouchData1; + +final GaugeTouchedSpot gaugeTouchedSpot1 = GaugeTouchedSpot( + const FlSpot(0, 1), + const Offset(0, 1), +); + +final gaugeTouchedSpotClone1 = gaugeTouchedSpot1; + +final GaugeTouchedSpot gaugeTouchedSpot2 = GaugeTouchedSpot( + const FlSpot(1, 0), + const Offset(0, 1), +); + +final GaugeTouchedSpot gaugeTouchedSpot3 = GaugeTouchedSpot( + const FlSpot(0, 1), + const Offset(1, 0), +); + +final GaugeTouchData gaugeTouchData1 = GaugeTouchData( + enabled: true, + touchCallback: (_, __) {}, +); + +final GaugeTouchData gaugeTouchData2 = GaugeTouchData( + enabled: false, + touchCallback: (_, __) {}, + mouseCursorResolver: (_, __) => MouseCursor.defer, +); + +const GaugeColor gaugeColor1 = SimpleGaugeColor(color: Colors.black); + +final GaugeColor gaugeColor2 = VariableGaugeColor( + limits: [0.1, 0.5], + colors: [Colors.red, Colors.red, Colors.red], +); + +const GaugeTicks gaugeTicks1 = GaugeTicks( + color: Colors.blue, + count: 4, + margin: 7, + position: GaugeTickPosition.center, + radius: 4, + showChangingColorTicks: false, +); + +final GaugeChartData gaugeChartData1 = GaugeChartData( + startAngle: 25, + endAngle: 90, + strokeWidth: 45, + value: 0.2, + valueColor: gaugeColor1, + backgroundColor: Colors.amber, + strokeCap: StrokeCap.round, + ticks: gaugeTicks1, + touchData: gaugeTouchData1, +); + +final GaugeChartData gaugeChartData1Clone = gaugeChartData1.copyWith(); diff --git a/test/chart/gauge_chart/gauge_chart_data_test.dart b/test/chart/gauge_chart/gauge_chart_data_test.dart new file mode 100644 index 000000000..45fe99164 --- /dev/null +++ b/test/chart/gauge_chart/gauge_chart_data_test.dart @@ -0,0 +1,359 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../data_pool.dart'; + +void main() { + group('GaugeChart Data equality check', () { + test('GaugeChartData equality test', () { + /// object equality test + expect(gaugeChartData1 == gaugeChartData1Clone, true); + + expect( + gaugeChartData1 == gaugeChartData1Clone.copyWith(value: 0.5), + false, + ); + + expect( + gaugeChartData1 == + gaugeChartData1Clone.copyWith(backgroundColor: Colors.black), + false, + ); + + expect( + gaugeChartData1 == + gaugeChartData1Clone.copyWith( + borderData: FlBorderData( + show: true, + border: Border.all(color: Colors.green), + ), + ), + true, + ); + + expect( + gaugeChartData1 == + gaugeChartData1Clone.copyWith( + ticks: const GaugeTicks(color: Colors.white), + ), + false, + ); + + expect( + gaugeChartData1 == + gaugeChartData1Clone.copyWith( + strokeCap: StrokeCap.square, + ), + false, + ); + + expect( + gaugeChartData1 == + gaugeChartData1Clone.copyWith(gaugeTouchData: gaugeTouchData2), + false, + ); + + expect( + gaugeChartData1 == gaugeChartData1Clone.copyWith(startAngle: 0), + false, + ); + + expect( + gaugeChartData1 == gaugeChartData1Clone.copyWith(endAngle: 0), + false, + ); + + expect( + gaugeChartData1 == gaugeChartData1Clone.copyWith(strokeWidth: 7), + false, + ); + + expect( + gaugeChartData1 == + gaugeChartData1Clone.copyWith(valueColor: gaugeColor2), + false, + ); + }); + + test('GaugeColor equality test', () { + expect(gaugeColor1 == const SimpleGaugeColor(color: Colors.black), true); + expect(gaugeColor1 == const SimpleGaugeColor(color: Colors.red), false); + + expect( + gaugeColor2 == + VariableGaugeColor( + limits: [0.1, 0.5], + colors: [Colors.red, Colors.red, Colors.red], + ), + true, + ); + + expect( + gaugeColor2 == + VariableGaugeColor( + limits: [0.2, 0.5], + colors: [Colors.red, Colors.red, Colors.red], + ), + false, + ); + + expect( + gaugeColor2 == + VariableGaugeColor( + limits: [0.1, 0.5], + colors: [Colors.blue, Colors.red, Colors.red], + ), + false, + ); + + expect( + gaugeColor2 == + VariableGaugeColor( + limits: [0.1, 0.5, 0.6], + colors: [Colors.red, Colors.red, Colors.red, Colors.red], + ), + false, + ); + }); + + test('GaugeTicks equality test', () { + expect( + gaugeTicks1 == + const GaugeTicks( + color: Colors.blue, + count: 4, + margin: 7, + position: GaugeTickPosition.center, + radius: 4, + showChangingColorTicks: false, + ), + true, + ); + + expect( + gaugeTicks1 == + const GaugeTicks( + color: Colors.red, + count: 4, + margin: 7, + position: GaugeTickPosition.center, + radius: 4, + showChangingColorTicks: false, + ), + false, + ); + + expect( + gaugeTicks1 == + const GaugeTicks( + color: Colors.blue, + count: 5, + margin: 7, + position: GaugeTickPosition.center, + radius: 4, + showChangingColorTicks: false, + ), + false, + ); + + expect( + gaugeTicks1 == + const GaugeTicks( + color: Colors.blue, + count: 4, + margin: 8, + position: GaugeTickPosition.center, + radius: 4, + showChangingColorTicks: false, + ), + false, + ); + + expect( + gaugeTicks1 == + const GaugeTicks( + color: Colors.blue, + count: 4, + margin: 7, + position: GaugeTickPosition.inner, + radius: 4, + showChangingColorTicks: false, + ), + false, + ); + + expect( + gaugeTicks1 == + const GaugeTicks( + color: Colors.blue, + count: 4, + margin: 7, + position: GaugeTickPosition.center, + radius: 1, + showChangingColorTicks: false, + ), + false, + ); + + expect( + gaugeTicks1 == + const GaugeTicks( + color: Colors.blue, + count: 4, + margin: 7, + position: GaugeTickPosition.center, + radius: 4, + ), + false, + ); + }); + + test('GaugeTouchData equality test', () { + expect(gaugeTouchData1 == gaugeTouchData1Clone, true); + + expect(gaugeTouchData1 == gaugeTouchData2, false); + + expect( + gaugeTouchData1 == + GaugeTouchData( + enabled: true, + touchCallback: (_, __) {}, + ), + false, + ); + + expect( + gaugeTouchData1 == + GaugeTouchData( + enabled: true, + mouseCursorResolver: (_, __) => MouseCursor.uncontrolled, + ), + false, + ); + + expect( + gaugeTouchData1 == + GaugeTouchData( + enabled: true, + longPressDuration: Duration.zero, + ), + false, + ); + + expect( + gaugeTouchData1 == + GaugeTouchData( + enabled: true, + touchCallback: (_, __) {}, + mouseCursorResolver: (_, __) => MouseCursor.uncontrolled, + longPressDuration: Duration.zero, + ), + false, + ); + }); + + test('GaugeTouchedSpot equality test', () { + expect(gaugeTouchedSpot1 == gaugeTouchedSpotClone1, true); + expect(gaugeTouchedSpot1 == gaugeTouchedSpot2, false); + expect(gaugeTouchedSpot1 == gaugeTouchedSpot3, false); + }); + + test('GaugeChartDataTween lerp', () { + final a = GaugeChartData( + value: 0.7, + strokeWidth: 5, + startAngle: 0, + endAngle: 270, + valueColor: const SimpleGaugeColor(color: MockData.color0), + backgroundColor: MockData.color0, + strokeCap: StrokeCap.round, + ticks: const GaugeTicks( + color: MockData.color0, + count: 5, + margin: 7, + position: GaugeTickPosition.center, + radius: 7, + showChangingColorTicks: false, + ), + touchData: GaugeTouchData( + enabled: true, + touchCallback: (_, __) {}, + longPressDuration: const Duration(seconds: 7), + mouseCursorResolver: (_, __) => MouseCursor.defer, + ), + ); + + final b = GaugeChartData( + value: 0.3, + strokeWidth: 3, + startAngle: 20, + endAngle: 250, + valueColor: VariableGaugeColor( + limits: [0.4], + colors: [MockData.color0, MockData.color2], + ), + backgroundColor: MockData.color2, + strokeCap: StrokeCap.square, + ticks: const GaugeTicks( + color: MockData.color2, + count: 7, + margin: 9, + position: GaugeTickPosition.inner, + radius: 5, + ), + touchData: GaugeTouchData( + enabled: true, + touchCallback: (_, __) {}, + longPressDuration: const Duration(seconds: 7), + mouseCursorResolver: (_, __) => MouseCursor.defer, + ), + ); + + final data = GaugeChartDataTween(begin: a, end: b).lerp(0.5); + + expect(data.value, 0.5); + expect(data.strokeWidth, 4); + expect(data.startAngle, 10); + expect(data.endAngle, 260); + expect(data.valueColor.getColor(0.5), MockData.color1); + expect(data.valueColor.getColor(0.5), MockData.color1); + final colorTicks = (data.valueColor as ColoredTicksGenerator) + .getColoredTicks() + .toList(); + expect(colorTicks, [ColoredTick(0.4, MockData.color2.withOpacity(0.5))]); + expect(data.strokeCap, StrokeCap.square); + expect(data.ticks?.color, MockData.color1); + expect(data.ticks?.count, 6); + expect(data.ticks?.margin, 8); + expect(data.ticks?.position, GaugeTickPosition.inner); + expect(data.ticks?.radius, 6); + expect(data.ticks?.showChangingColorTicks, true); + expect(data.touchData, b.touchData); + }); + + test('GaugeColor lerp', () { + final a = VariableGaugeColor( + limits: [0.2, 0.5, 0.7], + colors: [MockData.color0, MockData.color1, MockData.color2, MockData.color3], + ); + final b = VariableGaugeColor( + limits: [0.3, 0.6, 0.8], + colors: [MockData.color6, MockData.color5, MockData.color4, MockData.color3], + ); + final color = GaugeColor.lerp(a, b, 0.2); + + expect(color is ColoredTicksGenerator, true); + final generator = color as ColoredTicksGenerator; + final ticks = generator.getColoredTicks().toList(); + expect(ticks, [ + ColoredTick(0.2, MockData.color1.withOpacity(0.8)), + ColoredTick(0.5, MockData.color2.withOpacity(0.8)), + ColoredTick(0.7, MockData.color3.withOpacity(0.8)), + ColoredTick(0.3, MockData.color5.withOpacity(0.2)), + ColoredTick(0.6, MockData.color4.withOpacity(0.2)), + ColoredTick(0.8, MockData.color3.withOpacity(0.2)), + ]); + }); + }); +} diff --git a/test/chart/gauge_chart/gauge_chart_painter_test.dart b/test/chart/gauge_chart/gauge_chart_painter_test.dart new file mode 100644 index 000000000..ece96b8cb --- /dev/null +++ b/test/chart/gauge_chart/gauge_chart_painter_test.dart @@ -0,0 +1,420 @@ +import 'dart:math'; + +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; +import 'package:fl_chart/src/chart/gauge_chart/gauge_chart_painter.dart'; +import 'package:fl_chart/src/utils/canvas_wrapper.dart'; +import 'package:fl_chart/src/utils/utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import '../data_pool.dart'; +import 'gauge_chart_painter_test.mocks.dart'; + +@GenerateMocks([Canvas, CanvasWrapper, BuildContext, Utils]) +void main() { + final utilsMainInstance = Utils(); + group('paint()', () { + test('test 1', () { + const viewSize = Size(400, 400); + final data = GaugeChartData( + startAngle: 0, + endAngle: 90, + valueColor: const SimpleGaugeColor(color: Colors.red), + strokeWidth: 2, + value: 0.5, + ); + final gaugePainter = GaugeChartPainter(); + final holder = PaintHolder(data, data, 1); + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.radians(any)).thenAnswer((realInvocation) => 0); + when(mockUtils.degrees(any)).thenAnswer((realInvocation) => 0); + + final mockBuildContext = MockBuildContext(); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + gaugePainter.paint( + mockBuildContext, + mockCanvasWrapper, + holder, + ); + + verify(mockCanvasWrapper.drawArc(any, any, any, any, any)).called(1); + Utils.changeInstance(utilsMainInstance); + }); + }); + + group('drawValue()', () { + test('only value', () { + const viewSize = Size(400, 400); + final data = GaugeChartData( + startAngle: 0, + endAngle: 90, + valueColor: const SimpleGaugeColor(color: MockData.color0), + strokeWidth: 2, + value: 0.5, + ); + final gaugePainter = GaugeChartPainter(); + final holder = PaintHolder(data, data, 1); + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.radians(any)).thenAnswer( + (realInvocation) => realInvocation.positionalArguments[0] as double, + ); + + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + final drawArcResults = >[]; + when( + mockCanvasWrapper.drawArc( + captureAny, + captureAny, + captureAny, + captureAny, + captureAny, + ), + ).thenAnswer((inv) { + drawArcResults.add({ + 'rect': inv.positionalArguments[0] as Rect, + 'start_angle': inv.positionalArguments[1] as double, + 'sweep_angle': inv.positionalArguments[2] as double, + 'use_center': inv.positionalArguments[3] as bool, + 'paint_color': (inv.positionalArguments[4] as Paint).color, + 'paint_stroke_width': + (inv.positionalArguments[4] as Paint).strokeWidth, + 'paint_stroke_cap': (inv.positionalArguments[4] as Paint).strokeCap, + }); + }); + + gaugePainter.drawValue(mockCanvasWrapper, holder); + + expect(drawArcResults.length, 1); + + expect( + drawArcResults[0]['rect'], + Rect.fromCircle( + center: const Offset(200, 200), + radius: 200 - data.strokeWidth / 2, + ), + ); + expect(drawArcResults[0]['start_angle'], 0); + expect( + drawArcResults[0]['sweep_angle'], + 45, + ); // (endAngle - startAngle) * value + expect(drawArcResults[0]['use_center'], false); + expect(drawArcResults[0]['paint_color'], MockData.color0); + expect(drawArcResults[0]['paint_stroke_width'], data.strokeWidth); + expect(drawArcResults[0]['paint_stroke_cap'], data.strokeCap); + + Utils.changeInstance(utilsMainInstance); + }); + + test('with background', () { + const viewSize = Size(400, 400); + final data = GaugeChartData( + startAngle: 0, + endAngle: 90, + valueColor: const SimpleGaugeColor(color: MockData.color0), + strokeWidth: 2, + value: 0.5, + backgroundColor: MockData.color1, + ); + final gaugePainter = GaugeChartPainter(); + final holder = PaintHolder(data, data, 1); + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.radians(any)).thenAnswer( + (realInvocation) => realInvocation.positionalArguments[0] as double, + ); + + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + final drawArcResults = >[]; + when( + mockCanvasWrapper.drawArc( + captureAny, + captureAny, + captureAny, + captureAny, + captureAny, + ), + ).thenAnswer((inv) { + drawArcResults.add({ + 'rect': inv.positionalArguments[0] as Rect, + 'start_angle': inv.positionalArguments[1] as double, + 'sweep_angle': inv.positionalArguments[2] as double, + 'use_center': inv.positionalArguments[3] as bool, + 'paint_color': (inv.positionalArguments[4] as Paint).color, + 'paint_stroke_width': + (inv.positionalArguments[4] as Paint).strokeWidth, + 'paint_stroke_cap': (inv.positionalArguments[4] as Paint).strokeCap, + }); + }); + + gaugePainter.drawValue(mockCanvasWrapper, holder); + + expect(drawArcResults.length, 2); + + // background + expect( + drawArcResults[1]['rect'], + Rect.fromCircle( + center: const Offset(200, 200), + radius: 200 - data.strokeWidth / 2, + ), + ); + expect(drawArcResults[0]['start_angle'], 0); + expect( + drawArcResults[0]['sweep_angle'], + 90, + ); + expect(drawArcResults[0]['use_center'], false); + expect(drawArcResults[0]['paint_color'], MockData.color1); + expect(drawArcResults[0]['paint_stroke_width'], data.strokeWidth); + expect(drawArcResults[0]['paint_stroke_cap'], data.strokeCap); + + // value + expect( + drawArcResults[1]['rect'], + Rect.fromCircle( + center: const Offset(200, 200), + radius: 200 - data.strokeWidth / 2, + ), + ); + expect(drawArcResults[1]['start_angle'], 0); + expect( + drawArcResults[1]['sweep_angle'], + 45, + ); // (endAngle - startAngle) * value + expect(drawArcResults[1]['use_center'], false); + expect(drawArcResults[1]['paint_color'], MockData.color0); + expect(drawArcResults[1]['paint_stroke_width'], data.strokeWidth); + expect(drawArcResults[1]['paint_stroke_cap'], data.strokeCap); + + Utils.changeInstance(utilsMainInstance); + }); + }); + + group('drawTicks()', () { + test('ticks without changing color ticks', () { + const viewSize = Size(400, 400); + const gaugeTicks = GaugeTicks( + count: 5, + color: MockData.color0, + radius: 4, + showChangingColorTicks: false, + position: GaugeTickPosition.center, + margin: 5, + ); + final data = GaugeChartData( + startAngle: 0, + endAngle: 90, + valueColor: const SimpleGaugeColor(color: MockData.color0), + strokeWidth: 2, + value: 0.5, + ticks: gaugeTicks, + ); + final gaugePainter = GaugeChartPainter(); + final holder = PaintHolder(data, data, 1); + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.radians(any)).thenAnswer( + (realInvocation) => realInvocation.positionalArguments[0] as double, + ); + + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + final drawCircleResults = >[]; + when( + mockCanvasWrapper.drawCircle(captureAny, captureAny, captureAny), + ).thenAnswer((inv) { + drawCircleResults.add({ + 'offset': inv.positionalArguments[0] as Offset, + 'radius': inv.positionalArguments[1] as double, + 'paint_color': (inv.positionalArguments[2] as Paint).color, + }); + }); + + gaugePainter.drawTicks(mockCanvasWrapper, holder); + + expect(drawCircleResults.length, 5); + + final radius = 200 - data.strokeWidth / 2; + final angle = Utils().radians(90 / 4); + for (var i = 0; i < drawCircleResults.length; i++) { + + final tickX = 200 + cos(angle * i) * radius; + final tickY = 200 + sin(angle * i) * radius; + + expect(drawCircleResults[i]['offset'], Offset(tickX, tickY)); + expect(drawCircleResults[i]['radius'], gaugeTicks.radius); + expect(drawCircleResults[i]['paint_color'], gaugeTicks.color); + } + + Utils.changeInstance(utilsMainInstance); + }); + + test('ticks with changing color ticks', () { + const viewSize = Size(400, 400); + const gaugeTicks = GaugeTicks( + count: 5, + color: MockData.color0, + radius: 4, + position: GaugeTickPosition.center, + margin: 5, + ); + final gaugeColor = VariableGaugeColor( + limits: [0.3, 0.5, 0.8], + colors: [MockData.color0, MockData.color1, MockData.color2, MockData.color3,], + ); + final data = GaugeChartData( + startAngle: 0, + endAngle: 270, + valueColor: gaugeColor, + strokeWidth: 2, + value: 0.5, + ticks: gaugeTicks, + ); + final gaugePainter = GaugeChartPainter(); + final holder = PaintHolder(data, data, 1); + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.radians(any)).thenAnswer( + (realInvocation) => realInvocation.positionalArguments[0] as double, + ); + + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + final drawCircleResults = >[]; + when( + mockCanvasWrapper.drawCircle(captureAny, captureAny, captureAny), + ).thenAnswer((inv) { + drawCircleResults.add({ + 'offset': inv.positionalArguments[0] as Offset, + 'radius': inv.positionalArguments[1] as double, + 'paint_color': (inv.positionalArguments[2] as Paint).color, + }); + }); + + gaugePainter.drawTicks(mockCanvasWrapper, holder); + + expect(drawCircleResults.length, 5 + 3); + + final radius = 200 - data.strokeWidth / 2; + final angle = Utils().radians(270 / 4); + for (var i = 0; i < 5; i++) { + final tickX = 200 + cos(angle * i) * radius; + final tickY = 200 + sin(angle * i) * radius; + + expect(drawCircleResults[i]['offset'], Offset(tickX, tickY), reason: 'index $i'); + expect(drawCircleResults[i]['radius'], gaugeTicks.radius); + expect(drawCircleResults[i]['paint_color'], gaugeTicks.color); + } + + for (var i = 0; i < 3; i++) { + final angle = 270 * gaugeColor.limits[i]; + final tickX = 200 + cos(angle) * radius; + final tickY = 200 + sin(angle) * radius; + + expect(drawCircleResults[5 + i]['offset'], Offset(tickX, tickY), reason: 'index $i'); + expect(drawCircleResults[5 + i]['radius'], gaugeTicks.radius); + expect(drawCircleResults[5 + i]['paint_color'], gaugeColor.colors[i + 1]); + } + + Utils.changeInstance(utilsMainInstance); + }); + }); + + group('Range', () { + test('test 1', () { + expect(Range.fromValues(1, 10).contains(0.5), false); + expect(Range.fromValues(0, 0.5).contains(0.5), true); + expect(Range.fromValues(1, 1.5).contains(1), true); + expect(Range.fromValues(5, 10).contains(7), true); + expect(Range.fromValues(1, 10).contains(11), false); + expect(Range.fromValues(1, 10).contains(0.5), false); + }); + }); + + group('DegreeAngleRange', () { + test('test 1', () { + expect(const DegreeAngleRange(24, 50).contains(30), true); + expect(const DegreeAngleRange(50, 24).contains(30), true); + expect(const DegreeAngleRange(45, -225).contains(100), false); + expect(const DegreeAngleRange(45, -225).contains(171), true); + expect(const DegreeAngleRange(0, 315).contains(-46), true); + }); + }); + + group('handleTouch()', () { + test('test 1', () { + const viewSize = Size(250, 250); + final data = GaugeChartData( + value: 0.7, + valueColor: const SimpleGaugeColor(color: MockData.color0), + strokeWidth: 30, + startAngle: 45, + endAngle: -225, + strokeCap: StrokeCap.round, + touchData: GaugeTouchData( + enabled: true, + touchCallback: (_, __) {}, + ), + ); + final gaugePainter = GaugeChartPainter(); + final holder = PaintHolder(data, data, 1); + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.radians(captureAny)).thenAnswer( + (realInvocation) => utilsMainInstance.radians(realInvocation.positionalArguments[0] as double), + ); + when(mockUtils.degrees(captureAny)).thenAnswer( + (realInvocation) => utilsMainInstance.degrees(realInvocation.positionalArguments[0] as double), + ); + + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + final drawCircleResults = >[]; + when( + mockCanvasWrapper.drawCircle(captureAny, captureAny, captureAny), + ).thenAnswer((inv) { + drawCircleResults.add({ + 'offset': inv.positionalArguments[0] as Offset, + 'radius': inv.positionalArguments[1] as double, + 'paint_color': (inv.positionalArguments[2] as Paint).color, + }); + }); + + expect(gaugePainter.handleTouch(const Offset(189, 217), viewSize, holder), null); + expect(gaugePainter.handleTouch(const Offset(52, 71), viewSize, holder), null); + expect(gaugePainter.handleTouch(const Offset(40, 184), viewSize, holder), null); + expect(gaugePainter.handleTouch(const Offset(156, 133), viewSize, holder), null); + final expected = GaugeTouchedSpot( + const FlSpot(196.4392853163202, 41.3553437839966), + const Offset(196.4, 41.4), + ); + + const offsets = [Offset(195, 209), Offset(33, 61), Offset(170, 26)]; + for (final offset in offsets) { + final touch = gaugePainter.handleTouch(offset, viewSize, holder); + expect(touch != null, true); + expect(touch!.offset.dx, closeTo(expected.offset.dx, 0.1)); + expect(touch.offset.dy, closeTo(expected.offset.dy, 0.1)); + expect(touch.spot.x, closeTo(expected.spot.x, 0.0001)); + expect(touch.spot.y, closeTo(expected.spot.y, 0.0001)); + } + + Utils.changeInstance(utilsMainInstance); + }); + }); +} diff --git a/test/chart/gauge_chart/gauge_chart_painter_test.mocks.dart b/test/chart/gauge_chart/gauge_chart_painter_test.mocks.dart new file mode 100644 index 000000000..e0f813eba --- /dev/null +++ b/test/chart/gauge_chart/gauge_chart_painter_test.mocks.dart @@ -0,0 +1,1339 @@ +// Mocks generated by Mockito 5.4.2 from annotations +// in fl_chart/test/chart/gauge_chart/gauge_chart_painter_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:typed_data' as _i5; +import 'dart:ui' as _i2; + +import 'package:fl_chart/fl_chart.dart' as _i7; +import 'package:fl_chart/src/utils/canvas_wrapper.dart' as _i6; +import 'package:fl_chart/src/utils/utils.dart' as _i8; +import 'package:flutter/cupertino.dart' as _i3; +import 'package:flutter/foundation.dart' as _i4; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeRect_0 extends _i1.SmartFake implements _i2.Rect { + _FakeRect_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeCanvas_1 extends _i1.SmartFake implements _i2.Canvas { + _FakeCanvas_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeSize_2 extends _i1.SmartFake implements _i2.Size { + _FakeSize_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWidget_3 extends _i1.SmartFake implements _i3.Widget { + _FakeWidget_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeInheritedWidget_4 extends _i1.SmartFake + implements _i3.InheritedWidget { + _FakeInheritedWidget_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeDiagnosticsNode_5 extends _i1.SmartFake + implements _i3.DiagnosticsNode { + _FakeDiagnosticsNode_5( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + + @override + String toString({ + _i4.TextTreeConfiguration? parentConfiguration, + _i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info, + }) => + super.toString(); +} + +class _FakeOffset_6 extends _i1.SmartFake implements _i2.Offset { + _FakeOffset_6( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeBorderSide_7 extends _i1.SmartFake implements _i3.BorderSide { + _FakeBorderSide_7( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeTextStyle_8 extends _i1.SmartFake implements _i3.TextStyle { + _FakeTextStyle_8( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +/// A class which mocks [Canvas]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCanvas extends _i1.Mock implements _i2.Canvas { + MockCanvas() { + _i1.throwOnMissingStub(this); + } + + @override + void save() => super.noSuchMethod( + Invocation.method( + #save, + [], + ), + returnValueForMissingStub: null, + ); + @override + void saveLayer( + _i2.Rect? bounds, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #saveLayer, + [ + bounds, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void restore() => super.noSuchMethod( + Invocation.method( + #restore, + [], + ), + returnValueForMissingStub: null, + ); + @override + void restoreToCount(int? count) => super.noSuchMethod( + Invocation.method( + #restoreToCount, + [count], + ), + returnValueForMissingStub: null, + ); + @override + int getSaveCount() => (super.noSuchMethod( + Invocation.method( + #getSaveCount, + [], + ), + returnValue: 0, + ) as int); + @override + void translate( + double? dx, + double? dy, + ) => + super.noSuchMethod( + Invocation.method( + #translate, + [ + dx, + dy, + ], + ), + returnValueForMissingStub: null, + ); + @override + void scale( + double? sx, [ + double? sy, + ]) => + super.noSuchMethod( + Invocation.method( + #scale, + [ + sx, + sy, + ], + ), + returnValueForMissingStub: null, + ); + @override + void rotate(double? radians) => super.noSuchMethod( + Invocation.method( + #rotate, + [radians], + ), + returnValueForMissingStub: null, + ); + @override + void skew( + double? sx, + double? sy, + ) => + super.noSuchMethod( + Invocation.method( + #skew, + [ + sx, + sy, + ], + ), + returnValueForMissingStub: null, + ); + @override + void transform(_i5.Float64List? matrix4) => super.noSuchMethod( + Invocation.method( + #transform, + [matrix4], + ), + returnValueForMissingStub: null, + ); + @override + _i5.Float64List getTransform() => (super.noSuchMethod( + Invocation.method( + #getTransform, + [], + ), + returnValue: _i5.Float64List(0), + ) as _i5.Float64List); + @override + void clipRect( + _i2.Rect? rect, { + _i2.ClipOp? clipOp = _i2.ClipOp.intersect, + bool? doAntiAlias = true, + }) => + super.noSuchMethod( + Invocation.method( + #clipRect, + [rect], + { + #clipOp: clipOp, + #doAntiAlias: doAntiAlias, + }, + ), + returnValueForMissingStub: null, + ); + @override + void clipRRect( + _i2.RRect? rrect, { + bool? doAntiAlias = true, + }) => + super.noSuchMethod( + Invocation.method( + #clipRRect, + [rrect], + {#doAntiAlias: doAntiAlias}, + ), + returnValueForMissingStub: null, + ); + @override + void clipPath( + _i2.Path? path, { + bool? doAntiAlias = true, + }) => + super.noSuchMethod( + Invocation.method( + #clipPath, + [path], + {#doAntiAlias: doAntiAlias}, + ), + returnValueForMissingStub: null, + ); + @override + _i2.Rect getLocalClipBounds() => (super.noSuchMethod( + Invocation.method( + #getLocalClipBounds, + [], + ), + returnValue: _FakeRect_0( + this, + Invocation.method( + #getLocalClipBounds, + [], + ), + ), + ) as _i2.Rect); + @override + _i2.Rect getDestinationClipBounds() => (super.noSuchMethod( + Invocation.method( + #getDestinationClipBounds, + [], + ), + returnValue: _FakeRect_0( + this, + Invocation.method( + #getDestinationClipBounds, + [], + ), + ), + ) as _i2.Rect); + @override + void drawColor( + _i2.Color? color, + _i2.BlendMode? blendMode, + ) => + super.noSuchMethod( + Invocation.method( + #drawColor, + [ + color, + blendMode, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawLine( + _i2.Offset? p1, + _i2.Offset? p2, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawLine, + [ + p1, + p2, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawPaint(_i2.Paint? paint) => super.noSuchMethod( + Invocation.method( + #drawPaint, + [paint], + ), + returnValueForMissingStub: null, + ); + @override + void drawRect( + _i2.Rect? rect, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawRect, + [ + rect, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawRRect( + _i2.RRect? rrect, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawRRect, + [ + rrect, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawDRRect( + _i2.RRect? outer, + _i2.RRect? inner, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawDRRect, + [ + outer, + inner, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawOval( + _i2.Rect? rect, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawOval, + [ + rect, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawCircle( + _i2.Offset? c, + double? radius, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawCircle, + [ + c, + radius, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawArc( + _i2.Rect? rect, + double? startAngle, + double? sweepAngle, + bool? useCenter, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawArc, + [ + rect, + startAngle, + sweepAngle, + useCenter, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawPath( + _i2.Path? path, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawPath, + [ + path, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawImage( + _i2.Image? image, + _i2.Offset? offset, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawImage, + [ + image, + offset, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawImageRect( + _i2.Image? image, + _i2.Rect? src, + _i2.Rect? dst, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawImageRect, + [ + image, + src, + dst, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawImageNine( + _i2.Image? image, + _i2.Rect? center, + _i2.Rect? dst, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawImageNine, + [ + image, + center, + dst, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawPicture(_i2.Picture? picture) => super.noSuchMethod( + Invocation.method( + #drawPicture, + [picture], + ), + returnValueForMissingStub: null, + ); + @override + void drawParagraph( + _i2.Paragraph? paragraph, + _i2.Offset? offset, + ) => + super.noSuchMethod( + Invocation.method( + #drawParagraph, + [ + paragraph, + offset, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawPoints( + _i2.PointMode? pointMode, + List<_i2.Offset>? points, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawPoints, + [ + pointMode, + points, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawRawPoints( + _i2.PointMode? pointMode, + _i5.Float32List? points, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawRawPoints, + [ + pointMode, + points, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawVertices( + _i2.Vertices? vertices, + _i2.BlendMode? blendMode, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawVertices, + [ + vertices, + blendMode, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawAtlas( + _i2.Image? atlas, + List<_i2.RSTransform>? transforms, + List<_i2.Rect>? rects, + List<_i2.Color>? colors, + _i2.BlendMode? blendMode, + _i2.Rect? cullRect, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawAtlas, + [ + atlas, + transforms, + rects, + colors, + blendMode, + cullRect, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawRawAtlas( + _i2.Image? atlas, + _i5.Float32List? rstTransforms, + _i5.Float32List? rects, + _i5.Int32List? colors, + _i2.BlendMode? blendMode, + _i2.Rect? cullRect, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawRawAtlas, + [ + atlas, + rstTransforms, + rects, + colors, + blendMode, + cullRect, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawShadow( + _i2.Path? path, + _i2.Color? color, + double? elevation, + bool? transparentOccluder, + ) => + super.noSuchMethod( + Invocation.method( + #drawShadow, + [ + path, + color, + elevation, + transparentOccluder, + ], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [CanvasWrapper]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCanvasWrapper extends _i1.Mock implements _i6.CanvasWrapper { + MockCanvasWrapper() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.Canvas get canvas => (super.noSuchMethod( + Invocation.getter(#canvas), + returnValue: _FakeCanvas_1( + this, + Invocation.getter(#canvas), + ), + ) as _i2.Canvas); + @override + _i2.Size get size => (super.noSuchMethod( + Invocation.getter(#size), + returnValue: _FakeSize_2( + this, + Invocation.getter(#size), + ), + ) as _i2.Size); + @override + void drawRRect( + _i2.RRect? rrect, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawRRect, + [ + rrect, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void save() => super.noSuchMethod( + Invocation.method( + #save, + [], + ), + returnValueForMissingStub: null, + ); + @override + void restore() => super.noSuchMethod( + Invocation.method( + #restore, + [], + ), + returnValueForMissingStub: null, + ); + @override + void clipRect( + _i2.Rect? rect, { + _i2.ClipOp? clipOp = _i2.ClipOp.intersect, + bool? doAntiAlias = true, + }) => + super.noSuchMethod( + Invocation.method( + #clipRect, + [rect], + { + #clipOp: clipOp, + #doAntiAlias: doAntiAlias, + }, + ), + returnValueForMissingStub: null, + ); + @override + void translate( + double? dx, + double? dy, + ) => + super.noSuchMethod( + Invocation.method( + #translate, + [ + dx, + dy, + ], + ), + returnValueForMissingStub: null, + ); + @override + void rotate(double? radius) => super.noSuchMethod( + Invocation.method( + #rotate, + [radius], + ), + returnValueForMissingStub: null, + ); + @override + void drawPath( + _i2.Path? path, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawPath, + [ + path, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void saveLayer( + _i2.Rect? bounds, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #saveLayer, + [ + bounds, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawPicture(_i2.Picture? picture) => super.noSuchMethod( + Invocation.method( + #drawPicture, + [picture], + ), + returnValueForMissingStub: null, + ); + @override + void drawImage( + _i2.Image? image, + _i2.Offset? offset, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawImage, + [ + image, + offset, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void clipPath( + _i2.Path? path, { + bool? doAntiAlias = true, + }) => + super.noSuchMethod( + Invocation.method( + #clipPath, + [path], + {#doAntiAlias: doAntiAlias}, + ), + returnValueForMissingStub: null, + ); + @override + void drawRect( + _i2.Rect? rect, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawRect, + [ + rect, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawLine( + _i2.Offset? p1, + _i2.Offset? p2, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawLine, + [ + p1, + p2, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawCircle( + _i2.Offset? center, + double? radius, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawCircle, + [ + center, + radius, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawArc( + _i2.Rect? rect, + double? startAngle, + double? sweepAngle, + bool? useCenter, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawArc, + [ + rect, + startAngle, + sweepAngle, + useCenter, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawText( + _i3.TextPainter? tp, + _i2.Offset? offset, [ + double? rotateAngle, + ]) => + super.noSuchMethod( + Invocation.method( + #drawText, + [ + tp, + offset, + rotateAngle, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawDot( + _i7.FlDotPainter? painter, + _i7.FlSpot? spot, + _i2.Offset? offset, + ) => + super.noSuchMethod( + Invocation.method( + #drawDot, + [ + painter, + spot, + offset, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawRotated({ + required _i2.Size? size, + _i2.Offset? rotationOffset = _i2.Offset.zero, + _i2.Offset? drawOffset = _i2.Offset.zero, + required double? angle, + required _i6.DrawCallback? drawCallback, + }) => + super.noSuchMethod( + Invocation.method( + #drawRotated, + [], + { + #size: size, + #rotationOffset: rotationOffset, + #drawOffset: drawOffset, + #angle: angle, + #drawCallback: drawCallback, + }, + ), + returnValueForMissingStub: null, + ); + @override + void drawDashedLine( + _i2.Offset? from, + _i2.Offset? to, + _i2.Paint? painter, + List? dashArray, + ) => + super.noSuchMethod( + Invocation.method( + #drawDashedLine, + [ + from, + to, + painter, + dashArray, + ], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [BuildContext]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockBuildContext extends _i1.Mock implements _i3.BuildContext { + MockBuildContext() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Widget get widget => (super.noSuchMethod( + Invocation.getter(#widget), + returnValue: _FakeWidget_3( + this, + Invocation.getter(#widget), + ), + ) as _i3.Widget); + @override + bool get mounted => (super.noSuchMethod( + Invocation.getter(#mounted), + returnValue: false, + ) as bool); + @override + bool get debugDoingBuild => (super.noSuchMethod( + Invocation.getter(#debugDoingBuild), + returnValue: false, + ) as bool); + @override + _i3.InheritedWidget dependOnInheritedElement( + _i3.InheritedElement? ancestor, { + Object? aspect, + }) => + (super.noSuchMethod( + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + returnValue: _FakeInheritedWidget_4( + this, + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + ), + ) as _i3.InheritedWidget); + @override + void visitAncestorElements(_i3.ConditionalElementVisitor? visitor) => + super.noSuchMethod( + Invocation.method( + #visitAncestorElements, + [visitor], + ), + returnValueForMissingStub: null, + ); + @override + void visitChildElements(_i3.ElementVisitor? visitor) => super.noSuchMethod( + Invocation.method( + #visitChildElements, + [visitor], + ), + returnValueForMissingStub: null, + ); + @override + void dispatchNotification(_i3.Notification? notification) => + super.noSuchMethod( + Invocation.method( + #dispatchNotification, + [notification], + ), + returnValueForMissingStub: null, + ); + @override + _i3.DiagnosticsNode describeElement( + String? name, { + _i4.DiagnosticsTreeStyle? style = _i4.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method( + #describeElement, + [name], + {#style: style}, + ), + returnValue: _FakeDiagnosticsNode_5( + this, + Invocation.method( + #describeElement, + [name], + {#style: style}, + ), + ), + ) as _i3.DiagnosticsNode); + @override + _i3.DiagnosticsNode describeWidget( + String? name, { + _i4.DiagnosticsTreeStyle? style = _i4.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method( + #describeWidget, + [name], + {#style: style}, + ), + returnValue: _FakeDiagnosticsNode_5( + this, + Invocation.method( + #describeWidget, + [name], + {#style: style}, + ), + ), + ) as _i3.DiagnosticsNode); + @override + List<_i3.DiagnosticsNode> describeMissingAncestor( + {required Type? expectedAncestorType}) => + (super.noSuchMethod( + Invocation.method( + #describeMissingAncestor, + [], + {#expectedAncestorType: expectedAncestorType}, + ), + returnValue: <_i3.DiagnosticsNode>[], + ) as List<_i3.DiagnosticsNode>); + @override + _i3.DiagnosticsNode describeOwnershipChain(String? name) => + (super.noSuchMethod( + Invocation.method( + #describeOwnershipChain, + [name], + ), + returnValue: _FakeDiagnosticsNode_5( + this, + Invocation.method( + #describeOwnershipChain, + [name], + ), + ), + ) as _i3.DiagnosticsNode); +} + +/// A class which mocks [Utils]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockUtils extends _i1.Mock implements _i8.Utils { + MockUtils() { + _i1.throwOnMissingStub(this); + } + + @override + double radians(double? degrees) => (super.noSuchMethod( + Invocation.method( + #radians, + [degrees], + ), + returnValue: 0.0, + ) as double); + @override + double degrees(double? radians) => (super.noSuchMethod( + Invocation.method( + #degrees, + [radians], + ), + returnValue: 0.0, + ) as double); + @override + _i2.Size getDefaultSize(_i2.Size? screenSize) => (super.noSuchMethod( + Invocation.method( + #getDefaultSize, + [screenSize], + ), + returnValue: _FakeSize_2( + this, + Invocation.method( + #getDefaultSize, + [screenSize], + ), + ), + ) as _i2.Size); + @override + double translateRotatedPosition( + double? size, + double? degree, + ) => + (super.noSuchMethod( + Invocation.method( + #translateRotatedPosition, + [ + size, + degree, + ], + ), + returnValue: 0.0, + ) as double); + @override + _i2.Offset calculateRotationOffset( + _i2.Size? size, + double? degree, + ) => + (super.noSuchMethod( + Invocation.method( + #calculateRotationOffset, + [ + size, + degree, + ], + ), + returnValue: _FakeOffset_6( + this, + Invocation.method( + #calculateRotationOffset, + [ + size, + degree, + ], + ), + ), + ) as _i2.Offset); + @override + _i3.BorderRadius? normalizeBorderRadius( + _i3.BorderRadius? borderRadius, + double? width, + ) => + (super.noSuchMethod(Invocation.method( + #normalizeBorderRadius, + [ + borderRadius, + width, + ], + )) as _i3.BorderRadius?); + @override + _i3.BorderSide normalizeBorderSide( + _i3.BorderSide? borderSide, + double? width, + ) => + (super.noSuchMethod( + Invocation.method( + #normalizeBorderSide, + [ + borderSide, + width, + ], + ), + returnValue: _FakeBorderSide_7( + this, + Invocation.method( + #normalizeBorderSide, + [ + borderSide, + width, + ], + ), + ), + ) as _i3.BorderSide); + @override + double getEfficientInterval( + double? axisViewSize, + double? diffInAxis, { + double? pixelPerInterval = 40.0, + }) => + (super.noSuchMethod( + Invocation.method( + #getEfficientInterval, + [ + axisViewSize, + diffInAxis, + ], + {#pixelPerInterval: pixelPerInterval}, + ), + returnValue: 0.0, + ) as double); + @override + double roundInterval(double? input) => (super.noSuchMethod( + Invocation.method( + #roundInterval, + [input], + ), + returnValue: 0.0, + ) as double); + @override + String formatNumber(double? number) => (super.noSuchMethod( + Invocation.method( + #formatNumber, + [number], + ), + returnValue: '', + ) as String); + @override + _i3.TextStyle getThemeAwareTextStyle( + _i3.BuildContext? context, + _i3.TextStyle? providedStyle, + ) => + (super.noSuchMethod( + Invocation.method( + #getThemeAwareTextStyle, + [ + context, + providedStyle, + ], + ), + returnValue: _FakeTextStyle_8( + this, + Invocation.method( + #getThemeAwareTextStyle, + [ + context, + providedStyle, + ], + ), + ), + ) as _i3.TextStyle); + @override + double getBestInitialIntervalValue( + double? min, + double? max, + double? interval, { + double? baseline = 0.0, + }) => + (super.noSuchMethod( + Invocation.method( + #getBestInitialIntervalValue, + [ + min, + max, + interval, + ], + {#baseline: baseline}, + ), + returnValue: 0.0, + ) as double); + @override + double convertRadiusToSigma(double? radius) => (super.noSuchMethod( + Invocation.method( + #convertRadiusToSigma, + [radius], + ), + returnValue: 0.0, + ) as double); +} diff --git a/test/chart/gauge_chart/gauge_chart_renderer_test.dart b/test/chart/gauge_chart/gauge_chart_renderer_test.dart new file mode 100644 index 000000000..9353a3091 --- /dev/null +++ b/test/chart/gauge_chart/gauge_chart_renderer_test.dart @@ -0,0 +1,134 @@ + +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; +import 'package:fl_chart/src/chart/gauge_chart/gauge_chart_painter.dart'; +import 'package:fl_chart/src/chart/gauge_chart/gauge_chart_renderer.dart'; +import 'package:fl_chart/src/utils/canvas_wrapper.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import '../data_pool.dart'; +import 'gauge_chart_renderer_test.mocks.dart'; + +@GenerateMocks([Canvas, PaintingContext, BuildContext, GaugeChartPainter]) +void main() { + group('GaugeChartRenderer', () { + final data = GaugeChartData( + startAngle: 0, + endAngle: 270, + strokeWidth: 5, + value: 0.7, + valueColor: const SimpleGaugeColor(color: MockData.color0), + backgroundColor: MockData.color6, + strokeCap: StrokeCap.round, + ticks: const GaugeTicks( + color: MockData.color0, + margin: 5, + showChangingColorTicks: false, + radius: 4, + count: 5, + position: GaugeTickPosition.inner, + ), + ); + + final targetData = GaugeChartData( + startAngle: 0, + endAngle: 180, + strokeWidth: 5, + value: 0.5, + valueColor: const SimpleGaugeColor(color: MockData.color1), + backgroundColor: MockData.color5, + strokeCap: StrokeCap.round, + ticks: const GaugeTicks( + color: MockData.color1, + margin: 10, + radius: 20, + count: 30, + position: GaugeTickPosition.center, + ), + ); + + const textScale = 4.0; + + final mockBuildContext = MockBuildContext(); + final renderGaugeChart = RenderGaugeChart( + mockBuildContext, + data, + targetData, + textScale, + ); + + final mockPainter = MockGaugeChartPainter(); + final mockPaintingContext = MockPaintingContext(); + final mockCanvas = MockCanvas(); + const mockSize = Size(44, 44); + when(mockPaintingContext.canvas).thenAnswer((realInvocation) => mockCanvas); + renderGaugeChart + ..mockTestSize = mockSize + ..painter = mockPainter; + + test('test 1 correct data', () { + expect(renderGaugeChart.data == data, true); + expect(renderGaugeChart.data == targetData, false); + expect(renderGaugeChart.targetData == targetData, true); + expect(renderGaugeChart.textScale == textScale, true); + expect(renderGaugeChart.paintHolder.data == data, true); + expect(renderGaugeChart.paintHolder.targetData == targetData, true); + expect(renderGaugeChart.paintHolder.textScale == textScale, true); + }); + + test('test 2 check paint function', () { + renderGaugeChart.paint(mockPaintingContext, const Offset(10, 10)); + verify(mockCanvas.save()).called(1); + verify(mockCanvas.translate(10, 10)).called(1); + final result = verify(mockPainter.paint(any, captureAny, captureAny)); + expect(result.callCount, 1); + + final canvasWrapper = result.captured[0] as CanvasWrapper; + expect(canvasWrapper.size, const Size(44, 44)); + expect(canvasWrapper.canvas, mockCanvas); + + final paintHolder = result.captured[1] as PaintHolder; + expect(paintHolder.data, data); + expect(paintHolder.targetData, targetData); + expect(paintHolder.textScale, textScale); + + verify(mockCanvas.restore()).called(1); + }); + + test('test 3 check getResponseAtLocation function', () { + final results = >[]; + when(mockPainter.handleTouch(captureAny, captureAny, captureAny)) + .thenAnswer((inv) { + results.add({ + 'local_position': inv.positionalArguments[0] as Offset, + 'size': inv.positionalArguments[1] as Size, + 'paint_holder': inv.positionalArguments[2] as PaintHolder, + }); + return gaugeTouchedSpot1; + }); + final touchResponse = + renderGaugeChart.getResponseAtLocation(MockData.offset1); + expect(touchResponse.spot, gaugeTouchedSpot1); + expect(results[0]['local_position'] as Offset, MockData.offset1); + expect(results[0]['size'] as Size, mockSize); + final paintHolder = results[0]['paint_holder'] as PaintHolder; + expect(paintHolder.data, data); + expect(paintHolder.targetData, targetData); + expect(paintHolder.textScale, textScale); + }); + + test('test 4 check setters', () { + renderGaugeChart + ..data = targetData + ..targetData = data + ..textScale = 22; + + expect(renderGaugeChart.data, targetData); + expect(renderGaugeChart.targetData, data); + expect(renderGaugeChart.textScale, 22); + }); + }); +} diff --git a/test/chart/gauge_chart/gauge_chart_renderer_test.mocks.dart b/test/chart/gauge_chart/gauge_chart_renderer_test.mocks.dart new file mode 100644 index 000000000..74a1127a6 --- /dev/null +++ b/test/chart/gauge_chart/gauge_chart_renderer_test.mocks.dart @@ -0,0 +1,1270 @@ +// Mocks generated by Mockito 5.4.2 from annotations +// in fl_chart/test/chart/gauge_chart/gauge_chart_renderer_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:typed_data' as _i7; +import 'dart:ui' as _i2; + +import 'package:fl_chart/fl_chart.dart' as _i13; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart' + as _i12; +import 'package:fl_chart/src/chart/gauge_chart/gauge_chart_painter.dart' + as _i10; +import 'package:fl_chart/src/utils/canvas_wrapper.dart' as _i11; +import 'package:flutter/foundation.dart' as _i5; +import 'package:flutter/gestures.dart' as _i8; +import 'package:flutter/material.dart' as _i6; +import 'package:flutter/rendering.dart' as _i3; +import 'package:flutter/src/rendering/layer.dart' as _i4; +import 'package:flutter/src/widgets/notification_listener.dart' as _i9; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeRect_0 extends _i1.SmartFake implements _i2.Rect { + _FakeRect_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeCanvas_1 extends _i1.SmartFake implements _i2.Canvas { + _FakeCanvas_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakePaintingContext_2 extends _i1.SmartFake + implements _i3.PaintingContext { + _FakePaintingContext_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeColorFilterLayer_3 extends _i1.SmartFake + implements _i4.ColorFilterLayer { + _FakeColorFilterLayer_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeOpacityLayer_4 extends _i1.SmartFake implements _i4.OpacityLayer { + _FakeOpacityLayer_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeWidget_5 extends _i1.SmartFake implements _i6.Widget { + _FakeWidget_5( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeInheritedWidget_6 extends _i1.SmartFake + implements _i6.InheritedWidget { + _FakeInheritedWidget_6( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeDiagnosticsNode_7 extends _i1.SmartFake + implements _i5.DiagnosticsNode { + _FakeDiagnosticsNode_7( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + + @override + String toString({ + _i5.TextTreeConfiguration? parentConfiguration, + _i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info, + }) => + super.toString(); +} + +class _FakeOffset_8 extends _i1.SmartFake implements _i2.Offset { + _FakeOffset_8( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [Canvas]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCanvas extends _i1.Mock implements _i2.Canvas { + MockCanvas() { + _i1.throwOnMissingStub(this); + } + + @override + void save() => super.noSuchMethod( + Invocation.method( + #save, + [], + ), + returnValueForMissingStub: null, + ); + @override + void saveLayer( + _i2.Rect? bounds, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #saveLayer, + [ + bounds, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void restore() => super.noSuchMethod( + Invocation.method( + #restore, + [], + ), + returnValueForMissingStub: null, + ); + @override + void restoreToCount(int? count) => super.noSuchMethod( + Invocation.method( + #restoreToCount, + [count], + ), + returnValueForMissingStub: null, + ); + @override + int getSaveCount() => (super.noSuchMethod( + Invocation.method( + #getSaveCount, + [], + ), + returnValue: 0, + ) as int); + @override + void translate( + double? dx, + double? dy, + ) => + super.noSuchMethod( + Invocation.method( + #translate, + [ + dx, + dy, + ], + ), + returnValueForMissingStub: null, + ); + @override + void scale( + double? sx, [ + double? sy, + ]) => + super.noSuchMethod( + Invocation.method( + #scale, + [ + sx, + sy, + ], + ), + returnValueForMissingStub: null, + ); + @override + void rotate(double? radians) => super.noSuchMethod( + Invocation.method( + #rotate, + [radians], + ), + returnValueForMissingStub: null, + ); + @override + void skew( + double? sx, + double? sy, + ) => + super.noSuchMethod( + Invocation.method( + #skew, + [ + sx, + sy, + ], + ), + returnValueForMissingStub: null, + ); + @override + void transform(_i7.Float64List? matrix4) => super.noSuchMethod( + Invocation.method( + #transform, + [matrix4], + ), + returnValueForMissingStub: null, + ); + @override + _i7.Float64List getTransform() => (super.noSuchMethod( + Invocation.method( + #getTransform, + [], + ), + returnValue: _i7.Float64List(0), + ) as _i7.Float64List); + @override + void clipRect( + _i2.Rect? rect, { + _i2.ClipOp? clipOp = _i2.ClipOp.intersect, + bool? doAntiAlias = true, + }) => + super.noSuchMethod( + Invocation.method( + #clipRect, + [rect], + { + #clipOp: clipOp, + #doAntiAlias: doAntiAlias, + }, + ), + returnValueForMissingStub: null, + ); + @override + void clipRRect( + _i2.RRect? rrect, { + bool? doAntiAlias = true, + }) => + super.noSuchMethod( + Invocation.method( + #clipRRect, + [rrect], + {#doAntiAlias: doAntiAlias}, + ), + returnValueForMissingStub: null, + ); + @override + void clipPath( + _i2.Path? path, { + bool? doAntiAlias = true, + }) => + super.noSuchMethod( + Invocation.method( + #clipPath, + [path], + {#doAntiAlias: doAntiAlias}, + ), + returnValueForMissingStub: null, + ); + @override + _i2.Rect getLocalClipBounds() => (super.noSuchMethod( + Invocation.method( + #getLocalClipBounds, + [], + ), + returnValue: _FakeRect_0( + this, + Invocation.method( + #getLocalClipBounds, + [], + ), + ), + ) as _i2.Rect); + @override + _i2.Rect getDestinationClipBounds() => (super.noSuchMethod( + Invocation.method( + #getDestinationClipBounds, + [], + ), + returnValue: _FakeRect_0( + this, + Invocation.method( + #getDestinationClipBounds, + [], + ), + ), + ) as _i2.Rect); + @override + void drawColor( + _i2.Color? color, + _i2.BlendMode? blendMode, + ) => + super.noSuchMethod( + Invocation.method( + #drawColor, + [ + color, + blendMode, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawLine( + _i2.Offset? p1, + _i2.Offset? p2, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawLine, + [ + p1, + p2, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawPaint(_i2.Paint? paint) => super.noSuchMethod( + Invocation.method( + #drawPaint, + [paint], + ), + returnValueForMissingStub: null, + ); + @override + void drawRect( + _i2.Rect? rect, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawRect, + [ + rect, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawRRect( + _i2.RRect? rrect, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawRRect, + [ + rrect, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawDRRect( + _i2.RRect? outer, + _i2.RRect? inner, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawDRRect, + [ + outer, + inner, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawOval( + _i2.Rect? rect, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawOval, + [ + rect, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawCircle( + _i2.Offset? c, + double? radius, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawCircle, + [ + c, + radius, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawArc( + _i2.Rect? rect, + double? startAngle, + double? sweepAngle, + bool? useCenter, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawArc, + [ + rect, + startAngle, + sweepAngle, + useCenter, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawPath( + _i2.Path? path, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawPath, + [ + path, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawImage( + _i2.Image? image, + _i2.Offset? offset, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawImage, + [ + image, + offset, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawImageRect( + _i2.Image? image, + _i2.Rect? src, + _i2.Rect? dst, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawImageRect, + [ + image, + src, + dst, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawImageNine( + _i2.Image? image, + _i2.Rect? center, + _i2.Rect? dst, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawImageNine, + [ + image, + center, + dst, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawPicture(_i2.Picture? picture) => super.noSuchMethod( + Invocation.method( + #drawPicture, + [picture], + ), + returnValueForMissingStub: null, + ); + @override + void drawParagraph( + _i2.Paragraph? paragraph, + _i2.Offset? offset, + ) => + super.noSuchMethod( + Invocation.method( + #drawParagraph, + [ + paragraph, + offset, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawPoints( + _i2.PointMode? pointMode, + List<_i2.Offset>? points, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawPoints, + [ + pointMode, + points, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawRawPoints( + _i2.PointMode? pointMode, + _i7.Float32List? points, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawRawPoints, + [ + pointMode, + points, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawVertices( + _i2.Vertices? vertices, + _i2.BlendMode? blendMode, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawVertices, + [ + vertices, + blendMode, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawAtlas( + _i2.Image? atlas, + List<_i2.RSTransform>? transforms, + List<_i2.Rect>? rects, + List<_i2.Color>? colors, + _i2.BlendMode? blendMode, + _i2.Rect? cullRect, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawAtlas, + [ + atlas, + transforms, + rects, + colors, + blendMode, + cullRect, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawRawAtlas( + _i2.Image? atlas, + _i7.Float32List? rstTransforms, + _i7.Float32List? rects, + _i7.Int32List? colors, + _i2.BlendMode? blendMode, + _i2.Rect? cullRect, + _i2.Paint? paint, + ) => + super.noSuchMethod( + Invocation.method( + #drawRawAtlas, + [ + atlas, + rstTransforms, + rects, + colors, + blendMode, + cullRect, + paint, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawShadow( + _i2.Path? path, + _i2.Color? color, + double? elevation, + bool? transparentOccluder, + ) => + super.noSuchMethod( + Invocation.method( + #drawShadow, + [ + path, + color, + elevation, + transparentOccluder, + ], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [PaintingContext]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPaintingContext extends _i1.Mock implements _i3.PaintingContext { + MockPaintingContext() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.Rect get estimatedBounds => (super.noSuchMethod( + Invocation.getter(#estimatedBounds), + returnValue: _FakeRect_0( + this, + Invocation.getter(#estimatedBounds), + ), + ) as _i2.Rect); + @override + _i2.Canvas get canvas => (super.noSuchMethod( + Invocation.getter(#canvas), + returnValue: _FakeCanvas_1( + this, + Invocation.getter(#canvas), + ), + ) as _i2.Canvas); + @override + void paintChild( + _i3.RenderObject? child, + _i2.Offset? offset, + ) => + super.noSuchMethod( + Invocation.method( + #paintChild, + [ + child, + offset, + ], + ), + returnValueForMissingStub: null, + ); + @override + void appendLayer(_i4.Layer? layer) => super.noSuchMethod( + Invocation.method( + #appendLayer, + [layer], + ), + returnValueForMissingStub: null, + ); + @override + _i2.VoidCallback addCompositionCallback(_i4.CompositionCallback? callback) => + (super.noSuchMethod( + Invocation.method( + #addCompositionCallback, + [callback], + ), + returnValue: () {}, + ) as _i2.VoidCallback); + @override + void stopRecordingIfNeeded() => super.noSuchMethod( + Invocation.method( + #stopRecordingIfNeeded, + [], + ), + returnValueForMissingStub: null, + ); + @override + void setIsComplexHint() => super.noSuchMethod( + Invocation.method( + #setIsComplexHint, + [], + ), + returnValueForMissingStub: null, + ); + @override + void setWillChangeHint() => super.noSuchMethod( + Invocation.method( + #setWillChangeHint, + [], + ), + returnValueForMissingStub: null, + ); + @override + void addLayer(_i4.Layer? layer) => super.noSuchMethod( + Invocation.method( + #addLayer, + [layer], + ), + returnValueForMissingStub: null, + ); + @override + void pushLayer( + _i4.ContainerLayer? childLayer, + _i3.PaintingContextCallback? painter, + _i2.Offset? offset, { + _i2.Rect? childPaintBounds, + }) => + super.noSuchMethod( + Invocation.method( + #pushLayer, + [ + childLayer, + painter, + offset, + ], + {#childPaintBounds: childPaintBounds}, + ), + returnValueForMissingStub: null, + ); + @override + _i3.PaintingContext createChildContext( + _i4.ContainerLayer? childLayer, + _i2.Rect? bounds, + ) => + (super.noSuchMethod( + Invocation.method( + #createChildContext, + [ + childLayer, + bounds, + ], + ), + returnValue: _FakePaintingContext_2( + this, + Invocation.method( + #createChildContext, + [ + childLayer, + bounds, + ], + ), + ), + ) as _i3.PaintingContext); + @override + _i4.ClipRectLayer? pushClipRect( + bool? needsCompositing, + _i2.Offset? offset, + _i2.Rect? clipRect, + _i3.PaintingContextCallback? painter, { + _i2.Clip? clipBehavior = _i2.Clip.hardEdge, + _i4.ClipRectLayer? oldLayer, + }) => + (super.noSuchMethod(Invocation.method( + #pushClipRect, + [ + needsCompositing, + offset, + clipRect, + painter, + ], + { + #clipBehavior: clipBehavior, + #oldLayer: oldLayer, + }, + )) as _i4.ClipRectLayer?); + @override + _i4.ClipRRectLayer? pushClipRRect( + bool? needsCompositing, + _i2.Offset? offset, + _i2.Rect? bounds, + _i2.RRect? clipRRect, + _i3.PaintingContextCallback? painter, { + _i2.Clip? clipBehavior = _i2.Clip.antiAlias, + _i4.ClipRRectLayer? oldLayer, + }) => + (super.noSuchMethod(Invocation.method( + #pushClipRRect, + [ + needsCompositing, + offset, + bounds, + clipRRect, + painter, + ], + { + #clipBehavior: clipBehavior, + #oldLayer: oldLayer, + }, + )) as _i4.ClipRRectLayer?); + @override + _i4.ClipPathLayer? pushClipPath( + bool? needsCompositing, + _i2.Offset? offset, + _i2.Rect? bounds, + _i2.Path? clipPath, + _i3.PaintingContextCallback? painter, { + _i2.Clip? clipBehavior = _i2.Clip.antiAlias, + _i4.ClipPathLayer? oldLayer, + }) => + (super.noSuchMethod(Invocation.method( + #pushClipPath, + [ + needsCompositing, + offset, + bounds, + clipPath, + painter, + ], + { + #clipBehavior: clipBehavior, + #oldLayer: oldLayer, + }, + )) as _i4.ClipPathLayer?); + @override + _i4.ColorFilterLayer pushColorFilter( + _i2.Offset? offset, + _i2.ColorFilter? colorFilter, + _i3.PaintingContextCallback? painter, { + _i4.ColorFilterLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushColorFilter, + [ + offset, + colorFilter, + painter, + ], + {#oldLayer: oldLayer}, + ), + returnValue: _FakeColorFilterLayer_3( + this, + Invocation.method( + #pushColorFilter, + [ + offset, + colorFilter, + painter, + ], + {#oldLayer: oldLayer}, + ), + ), + ) as _i4.ColorFilterLayer); + @override + _i4.TransformLayer? pushTransform( + bool? needsCompositing, + _i2.Offset? offset, + _i8.Matrix4? transform, + _i3.PaintingContextCallback? painter, { + _i4.TransformLayer? oldLayer, + }) => + (super.noSuchMethod(Invocation.method( + #pushTransform, + [ + needsCompositing, + offset, + transform, + painter, + ], + {#oldLayer: oldLayer}, + )) as _i4.TransformLayer?); + @override + _i4.OpacityLayer pushOpacity( + _i2.Offset? offset, + int? alpha, + _i3.PaintingContextCallback? painter, { + _i4.OpacityLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushOpacity, + [ + offset, + alpha, + painter, + ], + {#oldLayer: oldLayer}, + ), + returnValue: _FakeOpacityLayer_4( + this, + Invocation.method( + #pushOpacity, + [ + offset, + alpha, + painter, + ], + {#oldLayer: oldLayer}, + ), + ), + ) as _i4.OpacityLayer); + @override + void clipPathAndPaint( + _i2.Path? path, + _i2.Clip? clipBehavior, + _i2.Rect? bounds, + _i2.VoidCallback? painter, + ) => + super.noSuchMethod( + Invocation.method( + #clipPathAndPaint, + [ + path, + clipBehavior, + bounds, + painter, + ], + ), + returnValueForMissingStub: null, + ); + @override + void clipRRectAndPaint( + _i2.RRect? rrect, + _i2.Clip? clipBehavior, + _i2.Rect? bounds, + _i2.VoidCallback? painter, + ) => + super.noSuchMethod( + Invocation.method( + #clipRRectAndPaint, + [ + rrect, + clipBehavior, + bounds, + painter, + ], + ), + returnValueForMissingStub: null, + ); + @override + void clipRectAndPaint( + _i2.Rect? rect, + _i2.Clip? clipBehavior, + _i2.Rect? bounds, + _i2.VoidCallback? painter, + ) => + super.noSuchMethod( + Invocation.method( + #clipRectAndPaint, + [ + rect, + clipBehavior, + bounds, + painter, + ], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [BuildContext]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockBuildContext extends _i1.Mock implements _i6.BuildContext { + MockBuildContext() { + _i1.throwOnMissingStub(this); + } + + @override + _i6.Widget get widget => (super.noSuchMethod( + Invocation.getter(#widget), + returnValue: _FakeWidget_5( + this, + Invocation.getter(#widget), + ), + ) as _i6.Widget); + @override + bool get mounted => (super.noSuchMethod( + Invocation.getter(#mounted), + returnValue: false, + ) as bool); + @override + bool get debugDoingBuild => (super.noSuchMethod( + Invocation.getter(#debugDoingBuild), + returnValue: false, + ) as bool); + @override + _i6.InheritedWidget dependOnInheritedElement( + _i6.InheritedElement? ancestor, { + Object? aspect, + }) => + (super.noSuchMethod( + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + returnValue: _FakeInheritedWidget_6( + this, + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + ), + ) as _i6.InheritedWidget); + @override + void visitAncestorElements(_i6.ConditionalElementVisitor? visitor) => + super.noSuchMethod( + Invocation.method( + #visitAncestorElements, + [visitor], + ), + returnValueForMissingStub: null, + ); + @override + void visitChildElements(_i6.ElementVisitor? visitor) => super.noSuchMethod( + Invocation.method( + #visitChildElements, + [visitor], + ), + returnValueForMissingStub: null, + ); + @override + void dispatchNotification(_i9.Notification? notification) => + super.noSuchMethod( + Invocation.method( + #dispatchNotification, + [notification], + ), + returnValueForMissingStub: null, + ); + @override + _i5.DiagnosticsNode describeElement( + String? name, { + _i5.DiagnosticsTreeStyle? style = _i5.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method( + #describeElement, + [name], + {#style: style}, + ), + returnValue: _FakeDiagnosticsNode_7( + this, + Invocation.method( + #describeElement, + [name], + {#style: style}, + ), + ), + ) as _i5.DiagnosticsNode); + @override + _i5.DiagnosticsNode describeWidget( + String? name, { + _i5.DiagnosticsTreeStyle? style = _i5.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method( + #describeWidget, + [name], + {#style: style}, + ), + returnValue: _FakeDiagnosticsNode_7( + this, + Invocation.method( + #describeWidget, + [name], + {#style: style}, + ), + ), + ) as _i5.DiagnosticsNode); + @override + List<_i5.DiagnosticsNode> describeMissingAncestor( + {required Type? expectedAncestorType}) => + (super.noSuchMethod( + Invocation.method( + #describeMissingAncestor, + [], + {#expectedAncestorType: expectedAncestorType}, + ), + returnValue: <_i5.DiagnosticsNode>[], + ) as List<_i5.DiagnosticsNode>); + @override + _i5.DiagnosticsNode describeOwnershipChain(String? name) => + (super.noSuchMethod( + Invocation.method( + #describeOwnershipChain, + [name], + ), + returnValue: _FakeDiagnosticsNode_7( + this, + Invocation.method( + #describeOwnershipChain, + [name], + ), + ), + ) as _i5.DiagnosticsNode); +} + +/// A class which mocks [GaugeChartPainter]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockGaugeChartPainter extends _i1.Mock implements _i10.GaugeChartPainter { + MockGaugeChartPainter() { + _i1.throwOnMissingStub(this); + } + + @override + void paint( + _i6.BuildContext? context, + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.GaugeChartData>? holder, + ) => + super.noSuchMethod( + Invocation.method( + #paint, + [ + context, + canvasWrapper, + holder, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawTicks( + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.GaugeChartData>? holder, + ) => + super.noSuchMethod( + Invocation.method( + #drawTicks, + [ + canvasWrapper, + holder, + ], + ), + returnValueForMissingStub: null, + ); + @override + void drawValue( + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.GaugeChartData>? holder, + ) => + super.noSuchMethod( + Invocation.method( + #drawValue, + [ + canvasWrapper, + holder, + ], + ), + returnValueForMissingStub: null, + ); + @override + _i13.GaugeTouchedSpot? handleTouch( + _i2.Offset? touchedPoint, + _i2.Size? viewSize, + _i12.PaintHolder<_i13.GaugeChartData>? holder, + ) => + (super.noSuchMethod(Invocation.method( + #handleTouch, + [ + touchedPoint, + viewSize, + holder, + ], + )) as _i13.GaugeTouchedSpot?); + @override + _i2.Offset center(_i2.Size? size) => (super.noSuchMethod( + Invocation.method( + #center, + [size], + ), + returnValue: _FakeOffset_8( + this, + Invocation.method( + #center, + [size], + ), + ), + ) as _i2.Offset); + @override + double gaugeRadius(_i2.Size? size) => (super.noSuchMethod( + Invocation.method( + #gaugeRadius, + [size], + ), + returnValue: 0.0, + ) as double); +} diff --git a/test/utils/utils_test.mocks.dart b/test/utils/utils_test.mocks.dart index fe4c97927..3aa8f0cad 100644 --- a/test/utils/utils_test.mocks.dart +++ b/test/utils/utils_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.0 from annotations +// Mocks generated by Mockito 5.4.2 from annotations // in fl_chart/test/utils/utils_test.dart. // Do not manually edit this file. From 7f3ddc60f9b055c72c19dc8d389e573fedcc4b74 Mon Sep 17 00:00:00 2001 From: Florian ARNOULD Date: Sat, 5 Aug 2023 15:52:28 +0200 Subject: [PATCH 6/8] chore: format --- .../chart/gauge_chart/gauge_chart_data.dart | 31 +++++----- .../gauge_chart/gauge_chart_painter.dart | 1 - .../gauge_chart/gauge_chart_data_test.dart | 19 ++++-- .../gauge_chart/gauge_chart_painter_test.dart | 59 ++++++++++++++----- .../gauge_chart_renderer_test.dart | 3 +- 5 files changed, 76 insertions(+), 37 deletions(-) diff --git a/lib/src/chart/gauge_chart/gauge_chart_data.dart b/lib/src/chart/gauge_chart/gauge_chart_data.dart index 09a566701..6b56c1e66 100644 --- a/lib/src/chart/gauge_chart/gauge_chart_data.dart +++ b/lib/src/chart/gauge_chart/gauge_chart_data.dart @@ -65,7 +65,9 @@ class SimpleGaugeColor with EquatableMixin implements GaugeColor { List get props => [color]; } -class VariableGaugeColor with EquatableMixin implements GaugeColor, ColoredTicksGenerator { +class VariableGaugeColor + with EquatableMixin + implements GaugeColor, ColoredTicksGenerator { VariableGaugeColor({ required this.limits, required this.colors, @@ -144,7 +146,8 @@ class GaugeTicks with EquatableMixin { } @override - List get props => [count, radius, color, position, margin, showChangingColorTicks]; + List get props => + [count, radius, color, position, margin, showChangingColorTicks]; } class GaugeChartData extends BaseChartData with EquatableMixin { @@ -215,18 +218,18 @@ class GaugeChartData extends BaseChartData with EquatableMixin { @override List get props => [ - ticks, - strokeCap, - backgroundColor, - valueColor, - value, - strokeWidth, - startAngle, - endAngle, - gaugeTouchData, - ticks, - borderData - ]; + ticks, + strokeCap, + backgroundColor, + valueColor, + value, + strokeWidth, + startAngle, + endAngle, + gaugeTouchData, + ticks, + borderData + ]; } class GaugeTouchData extends FlTouchData { diff --git a/lib/src/chart/gauge_chart/gauge_chart_painter.dart b/lib/src/chart/gauge_chart/gauge_chart_painter.dart index 9afea557a..960ec5025 100644 --- a/lib/src/chart/gauge_chart/gauge_chart_painter.dart +++ b/lib/src/chart/gauge_chart/gauge_chart_painter.dart @@ -167,7 +167,6 @@ class GaugeChartPainter extends BaseChartPainter { false, _valuePaint, ); - } GaugeTouchedSpot? handleTouch( diff --git a/test/chart/gauge_chart/gauge_chart_data_test.dart b/test/chart/gauge_chart/gauge_chart_data_test.dart index 45fe99164..35180235a 100644 --- a/test/chart/gauge_chart/gauge_chart_data_test.dart +++ b/test/chart/gauge_chart/gauge_chart_data_test.dart @@ -318,9 +318,8 @@ void main() { expect(data.endAngle, 260); expect(data.valueColor.getColor(0.5), MockData.color1); expect(data.valueColor.getColor(0.5), MockData.color1); - final colorTicks = (data.valueColor as ColoredTicksGenerator) - .getColoredTicks() - .toList(); + final colorTicks = + (data.valueColor as ColoredTicksGenerator).getColoredTicks().toList(); expect(colorTicks, [ColoredTick(0.4, MockData.color2.withOpacity(0.5))]); expect(data.strokeCap, StrokeCap.square); expect(data.ticks?.color, MockData.color1); @@ -335,11 +334,21 @@ void main() { test('GaugeColor lerp', () { final a = VariableGaugeColor( limits: [0.2, 0.5, 0.7], - colors: [MockData.color0, MockData.color1, MockData.color2, MockData.color3], + colors: [ + MockData.color0, + MockData.color1, + MockData.color2, + MockData.color3 + ], ); final b = VariableGaugeColor( limits: [0.3, 0.6, 0.8], - colors: [MockData.color6, MockData.color5, MockData.color4, MockData.color3], + colors: [ + MockData.color6, + MockData.color5, + MockData.color4, + MockData.color3 + ], ); final color = GaugeColor.lerp(a, b, 0.2); diff --git a/test/chart/gauge_chart/gauge_chart_painter_test.dart b/test/chart/gauge_chart/gauge_chart_painter_test.dart index ece96b8cb..892009c64 100644 --- a/test/chart/gauge_chart/gauge_chart_painter_test.dart +++ b/test/chart/gauge_chart/gauge_chart_painter_test.dart @@ -130,7 +130,7 @@ void main() { final mockUtils = MockUtils(); Utils.changeInstance(mockUtils); when(mockUtils.radians(any)).thenAnswer( - (realInvocation) => realInvocation.positionalArguments[0] as double, + (realInvocation) => realInvocation.positionalArguments[0] as double, ); final mockCanvasWrapper = MockCanvasWrapper(); @@ -153,7 +153,7 @@ void main() { 'use_center': inv.positionalArguments[3] as bool, 'paint_color': (inv.positionalArguments[4] as Paint).color, 'paint_stroke_width': - (inv.positionalArguments[4] as Paint).strokeWidth, + (inv.positionalArguments[4] as Paint).strokeWidth, 'paint_stroke_cap': (inv.positionalArguments[4] as Paint).strokeCap, }); }); @@ -226,7 +226,7 @@ void main() { final mockUtils = MockUtils(); Utils.changeInstance(mockUtils); when(mockUtils.radians(any)).thenAnswer( - (realInvocation) => realInvocation.positionalArguments[0] as double, + (realInvocation) => realInvocation.positionalArguments[0] as double, ); final mockCanvasWrapper = MockCanvasWrapper(); @@ -250,7 +250,6 @@ void main() { final radius = 200 - data.strokeWidth / 2; final angle = Utils().radians(90 / 4); for (var i = 0; i < drawCircleResults.length; i++) { - final tickX = 200 + cos(angle * i) * radius; final tickY = 200 + sin(angle * i) * radius; @@ -273,7 +272,12 @@ void main() { ); final gaugeColor = VariableGaugeColor( limits: [0.3, 0.5, 0.8], - colors: [MockData.color0, MockData.color1, MockData.color2, MockData.color3,], + colors: [ + MockData.color0, + MockData.color1, + MockData.color2, + MockData.color3, + ], ); final data = GaugeChartData( startAngle: 0, @@ -288,7 +292,7 @@ void main() { final mockUtils = MockUtils(); Utils.changeInstance(mockUtils); when(mockUtils.radians(any)).thenAnswer( - (realInvocation) => realInvocation.positionalArguments[0] as double, + (realInvocation) => realInvocation.positionalArguments[0] as double, ); final mockCanvasWrapper = MockCanvasWrapper(); @@ -315,7 +319,11 @@ void main() { final tickX = 200 + cos(angle * i) * radius; final tickY = 200 + sin(angle * i) * radius; - expect(drawCircleResults[i]['offset'], Offset(tickX, tickY), reason: 'index $i'); + expect( + drawCircleResults[i]['offset'], + Offset(tickX, tickY), + reason: 'index $i', + ); expect(drawCircleResults[i]['radius'], gaugeTicks.radius); expect(drawCircleResults[i]['paint_color'], gaugeTicks.color); } @@ -325,9 +333,16 @@ void main() { final tickX = 200 + cos(angle) * radius; final tickY = 200 + sin(angle) * radius; - expect(drawCircleResults[5 + i]['offset'], Offset(tickX, tickY), reason: 'index $i'); + expect( + drawCircleResults[5 + i]['offset'], + Offset(tickX, tickY), + reason: 'index $i', + ); expect(drawCircleResults[5 + i]['radius'], gaugeTicks.radius); - expect(drawCircleResults[5 + i]['paint_color'], gaugeColor.colors[i + 1]); + expect( + drawCircleResults[5 + i]['paint_color'], + gaugeColor.colors[i + 1], + ); } Utils.changeInstance(utilsMainInstance); @@ -375,10 +390,12 @@ void main() { final mockUtils = MockUtils(); Utils.changeInstance(mockUtils); when(mockUtils.radians(captureAny)).thenAnswer( - (realInvocation) => utilsMainInstance.radians(realInvocation.positionalArguments[0] as double), + (realInvocation) => utilsMainInstance + .radians(realInvocation.positionalArguments[0] as double), ); when(mockUtils.degrees(captureAny)).thenAnswer( - (realInvocation) => utilsMainInstance.degrees(realInvocation.positionalArguments[0] as double), + (realInvocation) => utilsMainInstance + .degrees(realInvocation.positionalArguments[0] as double), ); final mockCanvasWrapper = MockCanvasWrapper(); @@ -395,10 +412,22 @@ void main() { }); }); - expect(gaugePainter.handleTouch(const Offset(189, 217), viewSize, holder), null); - expect(gaugePainter.handleTouch(const Offset(52, 71), viewSize, holder), null); - expect(gaugePainter.handleTouch(const Offset(40, 184), viewSize, holder), null); - expect(gaugePainter.handleTouch(const Offset(156, 133), viewSize, holder), null); + expect( + gaugePainter.handleTouch(const Offset(189, 217), viewSize, holder), + null, + ); + expect( + gaugePainter.handleTouch(const Offset(52, 71), viewSize, holder), + null, + ); + expect( + gaugePainter.handleTouch(const Offset(40, 184), viewSize, holder), + null, + ); + expect( + gaugePainter.handleTouch(const Offset(156, 133), viewSize, holder), + null, + ); final expected = GaugeTouchedSpot( const FlSpot(196.4392853163202, 41.3553437839966), const Offset(196.4, 41.4), diff --git a/test/chart/gauge_chart/gauge_chart_renderer_test.dart b/test/chart/gauge_chart/gauge_chart_renderer_test.dart index 9353a3091..eaa9f739c 100644 --- a/test/chart/gauge_chart/gauge_chart_renderer_test.dart +++ b/test/chart/gauge_chart/gauge_chart_renderer_test.dart @@ -1,4 +1,3 @@ - import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; import 'package:fl_chart/src/chart/gauge_chart/gauge_chart_painter.dart'; @@ -110,7 +109,7 @@ void main() { return gaugeTouchedSpot1; }); final touchResponse = - renderGaugeChart.getResponseAtLocation(MockData.offset1); + renderGaugeChart.getResponseAtLocation(MockData.offset1); expect(touchResponse.spot, gaugeTouchedSpot1); expect(results[0]['local_position'] as Offset, MockData.offset1); expect(results[0]['size'] as Size, mockSize); From 5037e80d9ad7b0cd16c8588fdcf68d7d951dbc93 Mon Sep 17 00:00:00 2001 From: Florian ARNOULD Date: Tue, 22 Aug 2023 14:52:47 +0200 Subject: [PATCH 7/8] chore: added the gauge icon --- example/assets/icons/ic_gauge_chart.svg | 3 +++ example/lib/presentation/resources/app_assets.dart | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 example/assets/icons/ic_gauge_chart.svg diff --git a/example/assets/icons/ic_gauge_chart.svg b/example/assets/icons/ic_gauge_chart.svg new file mode 100644 index 000000000..0147dab82 --- /dev/null +++ b/example/assets/icons/ic_gauge_chart.svg @@ -0,0 +1,3 @@ + + + diff --git a/example/lib/presentation/resources/app_assets.dart b/example/lib/presentation/resources/app_assets.dart index 000b5e5cf..c3d8aca33 100644 --- a/example/lib/presentation/resources/app_assets.dart +++ b/example/lib/presentation/resources/app_assets.dart @@ -14,7 +14,7 @@ class AppAssets { case ChartType.radar: return 'assets/icons/ic_radar_chart.svg'; case ChartType.gauge: - return 'assets/icons/ic_radar_chart.svg'; + return 'assets/icons/ic_gauge_chart.svg'; } } From c4d564762895410a4d0d25cd73224a96ec1bde5c Mon Sep 17 00:00:00 2001 From: Florian ARNOULD Date: Tue, 22 Aug 2023 15:32:35 +0200 Subject: [PATCH 8/8] chore: reformat --- lib/src/chart/gauge_chart/gauge_chart_data.dart | 2 +- test/chart/gauge_chart/gauge_chart_data_test.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/chart/gauge_chart/gauge_chart_data.dart b/lib/src/chart/gauge_chart/gauge_chart_data.dart index 6b56c1e66..bd7a0aed9 100644 --- a/lib/src/chart/gauge_chart/gauge_chart_data.dart +++ b/lib/src/chart/gauge_chart/gauge_chart_data.dart @@ -228,7 +228,7 @@ class GaugeChartData extends BaseChartData with EquatableMixin { endAngle, gaugeTouchData, ticks, - borderData + borderData, ]; } diff --git a/test/chart/gauge_chart/gauge_chart_data_test.dart b/test/chart/gauge_chart/gauge_chart_data_test.dart index 35180235a..62ee5ec81 100644 --- a/test/chart/gauge_chart/gauge_chart_data_test.dart +++ b/test/chart/gauge_chart/gauge_chart_data_test.dart @@ -338,7 +338,7 @@ void main() { MockData.color0, MockData.color1, MockData.color2, - MockData.color3 + MockData.color3, ], ); final b = VariableGaugeColor( @@ -347,7 +347,7 @@ void main() { MockData.color6, MockData.color5, MockData.color4, - MockData.color3 + MockData.color3, ], ); final color = GaugeColor.lerp(a, b, 0.2);